route:cache 가 걸리면 RouteServiceProvider 가 캐시 로드로 분기해 라우트 파일이
실행되지 않는다. routes/devtools.php 는 전역 함수 5개를 정의하고 핸들러가 호출했고,
boost 의 browser-logs 는 프로바이더 boot 등록분이라 setCompiledRoutes 교체로
폐기됐다. 7.0.6 의 RouteCacheHelper 가 12개 라이프사이클 지점에서 캐시를 자동
생성하면서, 확장을 한 번이라도 건드린 사이트에서 항상 재현됐다.
로직을 오토로드되는 App\Support\DevTools\* 로 옮기고 browser-logs URI 를 G7 가
직접 소유해 캐시에 함께 구워지게 했다. 게이트가 참조하던 App\Models\Setting 은
저장소에 없는 유물이라 ConfigRepositoryInterface 로 교체했다.
같은 부류의 재발은 정적 검사(route-file-local-function)로 차단하고, 재현이
in-process 로 불가능하므로 서브프로세스 하네스로 회귀를 고정했다.
develop 리베이스 완주 후 보류 항목 처리.
- ApiDocPipelineTest 의 예시값 갱신 단언을 병합 계약(타입 동일 시 보존 —
멱등)에 정합화. 예시값 최신성 단언과 멱등성 설계가 같은 상황에 정반대를
요구하던 stale test 로, 병합에서 의도 채택한 쪽은 멱등성이다.
- 환경설정 보안 탭 체크박스 2개의 label prop 을 Label 래핑+형제 Span 으로
교체. label prop 은 렌더되지 않아 설명 문구가 화면에 없었다.
- 확장 vite config 의 sourcemap: true 하드코딩이 --production 의
G7_BUILD_SOURCEMAP=0 주입을 조용히 무시해 배포본 dist 에 .map 과 dangling
참조가 남던 문제를 tosspayments·message_bizppurio 에서 정정하고 production
재빌드. template-security.md 에 게이트 형태를 명시하고 audit 룰
vite-sourcemap-env-gate(error) 를 신설해 재발을 차단한다.
- PHPUnit 12 대비: 접촉 테스트 파일의 docblock 메타데이터 6건 attribute 전환.
이슈 본연: API 레퍼런스의 미채움 마커 5종(실측 제외/TODO/필드 없음/대표 에러 없음)
1,194건을 코드에서 읽어 전수 채우고(0건), api:docgen 재생성이 사람이 채운 내용을
손실·열화시키던 멱등성 결함 4종(CRLF, 표 파이프 이스케이프, 에러 서술 보존, 중복
라우트명 키)을 근본 수정. --check 를 실측 제외와 드리프트가 구분되도록 재정의.
ParameterDescriber/ResourceFieldDescriber 에 leaf 폴백·SEO 스코프·공통 사전 확장.
파생 결함(문서 실측 중 발견): ResponseHelper 기본 메시지 키 6종이 존재하지 않는
messages.* 를 가리켜 응답 message 가 번역문 대신 키 문자열로 노출되던 문제를 common.*
으로 정정하고, 코어 7 + gdpr/marketing/pay_kginicis/verification/ecommerce 확장의
호출부와 누락 lang 키(게시판 10키 + 이커머스 category_images 4키 포함)를 ko/en/ja
전수 정정. 중복 그룹 키 2건은 기존 키로 호출부 통합.
보안: GET /api/identity/challenges/{id} 는 권한 가드 없는 공개 폴링 엔드포인트인데
Service::getStatus 가 attempts/max_attempts 를 응답에 담아 남은 시도 횟수를 추론할 수
있었다. 두 필드를 제거하고, 챌린지 화면(admin_basic/basic)의 서버 attempts 참조를
제거(남은 횟수 UI 는 query fallback + verify 실패 시 로컬 증가로 유지).
부수 정리(변경셋에 들어온 board 컨트롤러의 audit 사전 결함): FormRequest 4개 신설로
base Request 주입 제거, CommentController 의 Board 직접 호출을 BoardService 위임으로
전환, PHPDoc @return 보강. board 스위트에서 발견한 stale test 3건(seed SSoT 불일치,
admin 계정 user 역할 누락)도 같은 세션에서 정정.
버전 정렬(공개 release 기준): 코어 7.0.3→7.0.4, sirsoft-board 1.0.1→1.0.2,
verification_kginicis 1.0.0→1.0.1, admin_basic 1.0.1→1.0.4.
재생성이 사람이 채운 서술과 이전 실측 결과를 지우고, 코드가 바뀌지 않은
문서까지 매 실행마다 흔들던 결함 5건을 고쳤다.
- 중첩 객체 파라미터가 문서에서 통째로 누락되던 문제: 배열 요소(items.*.id)를
상위로 대표시키려던 스킵 조건이 점(.) 포함 여부만 봐서, 와일드카드가 없는
중첩 객체 필드(refund_bank.*, general.*, content.* 등 17개 엔드포인트 수백 개)
까지 함께 버렸다. 와일드카드가 있을 때만 스킵하도록 좁혔다.
- 사람이 보강한 에러 응답 표가 자동 추론 초안에 덮여 사라지던 문제:
상태코드 키 단위로 병합한다.
- 실측 실패가 이전에 관측해 둔 응답 예시를 지우던 문제: 실측 성패는 호출 시점
데이터 유무에 좌우되므로(DELETE /checkout 은 대상이 없으면 404), 실패는
"새로 관측하지 못했다"는 뜻이지 기존 관측이 무효라는 뜻이 아니다.
- 실측 예시값·응답 예시 JSON 이 DB 상태를 그대로 반영해 비멱등하던 문제:
값이 아니라 스펙(필드 집합·타입)이 바뀐 경우에만 갱신한다. nullable 이
null 로 관측된 것은 타입 변경이 아니므로 기존 값을 유지한다.
- 요청 예시 path 파라미터가 실측 성패에 따라 흔들리던 문제: placeholder 로 고정.
결과: 문서 63개 전수 대조에서 손실 0(서술·표 셀·응답 예시), 전 scope 연속
재생성 시 diff 0(멱등).
Checkbox 는 label prop 을 렌더하지 않는다(부모 Label 이 표시 담당). 그 prop 에만
의존해 결제수단 9종의 라벨이 화면에서 전부 비어 있었다. Label 로 감싸고 텍스트를
형제 Span 으로 분리했다 — 라벨 클릭 토글과 접근성 이름도 함께 확보된다.
수정 중 저장 결함을 함께 발견했다. 폼 자동바인딩은 값이 boolean 일 때만 checked 로
붙고, null 이면 value 분기로 떨어져 빈 문자열을 전송한다. 서버가 그것을 null 로
저장하면 기본값(false)을 덮어 화면에서 다시 켤 수 없게 고착된다. initLocal 로는
막을 수 없어(shallow 는 form 객체를 교체하고, deep 은 source 의 null 을 그대로
반영) 레이아웃이 boolean 을 직접 보장하도록 했다 — autoBinding:false + checked 강제
+ change 액션이 $event.target.checked 를 기록.
같은 원인으로 편집기 팔레트도 라벨 없는 체크박스를 삽입하고 있어 함께 고쳤다.
회귀 방어: 레이아웃/팔레트 계약 테스트, 엔진 동작(isCheckedBinding·deepMergeState)
고정, 저장 페이로드가 boolean 인지 검사하는 E2E, audit 룰
layout-checkbox-label-and-boolean(label prop=error, 자동바인딩 의존=warn).
현금영수증 발급사를 PG 결제사와 독립적으로 선택할 수 있도록 공용 훅 축
(registered_providers/issue/cancel)에 토스·KG 프로바이더를 등록한다. KG 결제로
받으면서 영수증만 토스로 발급하는 구성이 가능해진다. KG 전용 발행 컨트롤러·
라우트·관리자 패널은 이 축으로 이전하며 제거했다 — 이중 발행 경로를 남기지
않기 위해서다.
토스 환불은 네 가지를 바로잡았다. 전액취소를 부분취소로 보내던 문제(R1),
면세금액 미전달로 복합과세 상점의 부가세가 틀어지던 문제(R2), 가상계좌
환불계좌를 싣지 않아 취소가 실패하던 문제(R3), 에스크로 부분취소를 PG 호출
전에 차단하지 않던 문제(E4). 취소·발급 요청에는 멱등키를 부착해 네트워크
재시도가 국세청에 이중 신고되거나 이중 환불되는 것을 막는다.
코어 활동로그 가드가 \Exception 만 잡아 채널 해석 실패(\Error)를 놓치고 있었다.
결제 입금통보 같은 외부 콜백이 로그 문제만으로 500을 반환하던 문제를 \Throwable
로 수정한다. 활동로그는 부수 기록이므로 그것을 유발한 요청까지 죽여서는 안 된다.
토스 API 서비스는 베이스 \Exception 을 직접 던지고 있어 도메인 예외
(TossPaymentsApiException)를 신설했다. KG의 KgInicisApiException 선례와 동형이며
RuntimeException 상속이라 기존 소비자의 catch 는 그대로 동작한다.
토스페이먼츠 플러그인을 프로덕션 수준으로 완성한다 (계획서 S4).
주문서형 결제(9종 동적등록)
- 플러그인이 filter_available_payment_methods 로 결제수단을 주입하면 체크아웃에
독립 항목으로 노출된다. 서버 전송 시에는 core_payment_method 로 번역해 보낸다 —
코어 PaymentMethodEnum 은 toss_* 를 거부하므로 번역 없이는 주문 자체가 422 로 실패했다.
번역 근거인 core_payment_method 가 설정 병합/스냅샷의 화이트리스트에서 탈락하던 것을
provider-agnostic 하게 보존하도록 고쳤다 (토스 전용 분기 없음).
가상계좌·웹훅·에스크로
- 입금통보/결제상태 웹훅 2종 신설. secret 대조(hash_equals)·리플레이 멱등·CSRF 면제.
- 에스크로 3상태(off/on/buyer_choice). escrowProducts 를 가상계좌·계좌이체 양쪽
SDK 페이로드에 싣는다 — 서버가 조립해도 SDK 로 전달되지 않으면 토스가 결제를 거부한다.
E2E 가 SDK 경계를 직접 캡처해 이 계약의 나머지 절반을 잠근다.
- 관리자 설정 저장 시 입금기한·에스크로 값 범위를 서버에서 검증한다 (UI max 는 클라 힌트라
API 직접 호출을 막지 못한다).
레이아웃 리스너 실행 순서
- KG 는 no-PG 리스트를 통짜 리터럴로 str_replace 하므로 토스가 먼저 append 하면 KG 의
매치가 깨진다. HookManager 는 ksort 오름차순이므로 토스 priority 를 30 으로 두어
"KG(20) 먼저" 를 플러그인 로드 순서와 무관한 불변식으로 고정했다.
코어 — API 문서 생성기가 확장 web 라우트도 수집
- PG 콜백·웹훅은 CSRF/세션 특성상 web.php 에 등록되지만 외부 시스템이 호출하는
machine-facing 엔드포인트라 API 레퍼런스 대상이다. api/ prefix 만 수집하던 탓에
영구 무문서였다. 확장 소유 web 라우트를 수집하되 관리자 화면(admin 컨텍스트)은 제외한다.
코어 라우트 수집 결과는 291건으로 불변.
하네스 — 확장 테스트 베이스클래스 룰 신설
- coverage.json 의 test-extension-base-class 가 status:todo(룰 미구현)라 규정이 있어도
검출되지 않았고, 실제로 이번 신설 테스트가 그 사각에 빠졌다. 룰을 구현해 봉인한다.
무통장입금 주문의 현금영수증 발급을 관리자·회원·비회원 세 경로에서 제공하고,
입금이 확인되면 자동 발급하며, 부분환불로 금액이 바뀌면 전액취소 후 재발급한다.
발급 프로바이더에 종속되지 않도록 발급/취소는 필터 훅으로 위임한다.
자동발급 리스너는 결제완료 전이(after_payment_complete)와 입금만 기록
(after_deposit_recorded) 두 경로를 모두 구독한다 — 후자가 없으면 관리자가
"입금만 기록"했을 때 자동발급이 통째로 누락된다.
금액 동기화(syncFromOrder)는 트랜잭션이 닫힌 뒤에 부른다. 재발급 실패 이력은
국세청 신고 누락을 막는 원장이라 롤백에 휩쓸리면 안 되고, 프로바이더 왕복 2회
동안 DB 잠금을 붙들 수 없기 때문이다. 환불은 이미 확정됐으므로 어떤 실패도
호출부로 전파하지 않고 FAILED 이력 + 로그로 남겨 관리자 수동 복구에 맡긴다.
식별번호는 재발급용으로만 암호화 보관하고 구매확정 시점에 폐기한다.
그 시점을 잡기 위해 order.after_purchase_confirmed 훅을 신설했다.
함께 고친 사전 결함:
- 취소 시 total_cash_equivalent_amount 가 재계산되지 않아 syncFromOrder 가
"금액 변동 없음"으로 판정, 부분환불 후 재발급이 아예 동작하지 않았다.
산정 규칙을 PaymentMethodEnum::resolveCashEquivalentAmount 로 SSoT 화해
주문 생성 시점과 취소 재계산 시점이 같은 규칙을 쓰게 했다.
- 현금영수증 발급이 payment.receipt_url 을 덮어써 카드 매출전표 URL 이
유실됐다. 한 컬럼이 두 의미를 갖던 문제로, 현금영수증 URL 은 이력 테이블이
보관하도록 분리했다.
- OrderService::confirmOption 이 confirmed_at 을 무조건 now 로 덮어썼다
(v0.16.1 부터 존재). 재확정 시 최초 확정 시점이 소실되고 구매확정 훅이
중복 발화한다. OrderOptionService 와 대칭으로 멱등 가드를 넣었다.
- 코어 api:docgen 이 사람이 작성한 API 문서를 재생성 때마다 지웠다.
@generated 블록 안의 표 셀·응답 본문이 대상이었고, extractGeneratedBlock
이 앞의 `## ` 헤딩까지 거슬러 올라가 여러 엔드포인트를 한 덩어리로 반환하던
문제도 함께 고쳤다. 실측 산출 응답 예시에 @probed 출처 마커를 붙여, 마커가
없는 본문은 사람 작성분으로 보고 보존한다 — 실측 1회의 표본은 사람이 코드에서
읽어낸 사실보다 좁으므로 실측은 기본값이지 덮어쓰기 권한이 아니다.
ParameterDescriber 가 {order} 같은 path 파라미터를 "정렬 방향 asc/desc" 로
설명하던 오염도 정정했다.
토스페이먼츠 복원 후 api-doc-coverage 가 warn 4건을 냈으나, 토스는 src/routes/api.php
가 없고 web.php 의 브라우저 리다이렉트 콜백 2건만 가진다. api:docgen 은 이 경우
"API 라우트가 없습니다" 로 응답하므로 문서 생성 자체가 불가능하다. 즉 충족할 수 없는
위반을 요구하고 있었다.
원인은 룰의 appliesTo 가 routes/*.php·Controllers/** 를 파일 경로로만 매칭하고 그
확장이 실제 api/ 표면을 갖는지 확인하지 않은 것이다. 확장의 api/ prefix 라우트는
RouteServiceProvider 가 api.php 를 로드해서만 등록되므로(web.php 는 web prefix),
api.php 존재 여부가 필요충분 판정자다. 번들 확장 14개 전수 대조에서 docs/api 보유와
api.php 존재가 14/14 일치함을 확인했다.
extensionHasApiSurface 게이트를 두어 api.php 가 없는 확장을 check 단계에서
면제한다. 확장명은 하드코딩하지 않으므로 api.php 가 추가되면 자동으로 검사 대상이
된다. repoRoot 미주입 시에는 판정이 불가하므로 기존대로 보수적으로 보고한다.
같은 오탐이 gnuboard7-hello_plugin 에도 있었고 함께 해소된다. api.php 를 가진
pay_kginicis 는 web.php 만 바뀌어도 여전히 문서 동반을 요구한다(면제 오적용 방지).
규정과 도구를 함께 갱신했다 — api-documentation.md 에 "문서 대상이 아닌 확장" 절을
신설하고, coverage.json 의 stale 한 note(verification_kginicis 를 미문서화 대상으로
기술)를 실제 상태로 정정했다.
합의되지 않은 구조 변경을 되돌리고, 계획서 전수조사에서 드러난 결함을 처리했다.
쇼핑 화면 호출 통합(F4)은 API 쿼리 튜닝이 아니라 화면 구조 변경이라 원복했다.
레이아웃 9개를 develop 상태로 복원하고 전용 엔드포인트·컨트롤러·문서·테스트를 걷어냈다.
되돌리면 함께 사라지는 비-F4 수정 둘(페이저 has_more_pages 판정 3곳, Icon 크기 클래스
23줄)은 재적용했다. 성능 개선분은 전부 잔존한다.
통합검색 화면 상태를 전역 상태에서 URL 쿼리로 옮겼다. 종전에는 탭·필터·정렬·페이지가
주소에 없어 새로고침·뒤로가기에서 초기화되고 결과를 공유할 수도 없었다. 화면은 정상으로
보이고 콘솔 에러도 없어 코드만으로는 드러나지 않는 형태였다.
확장이 라우트를 추가해도 라우트 캐시가 걸린 사이트에서는 그 주소가 404 가 되던 구멍을
막았다. 훅 캐시와 달리 라우트 캐시에는 스캔 폴백이 없어 예외도 경고도 남지 않는다.
같은 문제를 이미 푼 ConfigCacheHelper 를 미러링해 갱신 지점을 한 곳으로 모았고,
코어 업데이트가 캐시를 비우기만 하고 되살리지 않던 문제도 함께 고쳤다.
상품·페이지 검색이 커서 판정에 페이지 번호를 넘기지 않아, 커서 없이 깊은 페이지를
지목한 딥링크가 조용히 1 페이지를 돌려주던 결함을 고쳤다. 호출 지점을 손으로 열거하는
대신 저장소를 훑는 패리티 테스트로 다음 도메인의 재발을 막는다.
환경설정 고급 탭의 목록 상한값이 저장되지 않던 결함을 고쳤다. 화면·검증·읽기는 모두
있었는데 저장 시 카테고리 분류표가 손으로 열거돼 있어 그 값만 버려지고 있었다. 분류표를
설정 정의에서 도출하도록 바꿔 다음 카테고리가 합류해도 같은 일이 생기지 않게 했다.
범위를 벗어난 값의 안내에 내부 식별자가 노출되던 것도 함께 정리했다.
언어팩 재설치가 활성 팩을 자기 자신과의 슬롯 충돌로 오인해 강등하던 회귀와, CLI 설치가
HTTP 권한 컨텍스트 없이 자동 활성화되는지를 고정하는 테스트를 함께 담았다.
공개 이슈 gnuboard/g7 이 지목한 병목은 "같은 일을 반복 실행한다" 축이다.
응답·행의 무게를 다룬 · 과 겹치지 않는다.
공통 계약 — 총 건수 상한과 페이지 이동 범위는 별개 결정이다. 묶으면 필요 없이
기능이 깎인다. 총 건수만 상한을 받고(파생 테이블 COUNT), "다음" 이동은
per_page + 1 실측으로 끝까지 열어 둔다. 계산이 불가능해지는 것은 마지막 페이지
번호 하나뿐이며 그 사실은 last_page: null 이 알린다. 최신순처럼 실제 컬럼으로
정렬하는 목록은 커서로 전환해 깊이와 무관하게 일정 속도로 이동한다. 관련도순은
계산값 정렬이라 커서 키로 쓸 수 없어 offset 을 유지한다.
계약의 입구는 표준 paginate 와 같은 폭이어야 한다. 관계·쿼리 빌더를 받지 못하면
그 좁은 만큼이 그대로 운영 500 이 되고, 실제로 관리자 알림 목록에서 그렇게 터졌다.
응답 조립도 컬렉션마다 손으로 하면 형태가 늘어나는 순간 없는 값을 부르거나 새
필드를 흘리므로, 형태 판정을 paginationMeta 한 곳에 모았다. 표준 paginate
응답은 필드 단위로 이전과 동일하다.
요청당 반복 비용 — 훅 구독마다 남기던 로그 400줄, 요청당 스무 번 넘던 설정 파일
재읽기, 이미 캐시된 목록의 DB 재조회를 없앴다. 권한 판정은 요청 스코프 메모를 두어
화면 요소마다 나가던 조회를 한 번으로 줄였고, 크로스 요청 캐시는 두지 않아 권한
변경이 종전처럼 다음 요청에 반영된다.
검색 질의는 활성 엔진이 만든다. 저장소가 구체 엔진을 지목하면 플러그인이 등록한
엔진은 호출될 기회 자체를 잃고 오류 없이 다른 방식으로 동작한다. 해석기를 두어
활성 엔진에 위임하고, 전문검색이 없는 DBMS 의 부분일치 폴백도 드라이버명 하드코딩
대신 선언형 config 로 옮겼다.
총 건수가 잘린 목록에서 순번을 역산하면 0 과 음수가 나온다. 지어내지 않고 null 을
돌려주며, 그 원칙을 last_page 와 동일하게 적용했다.
관리자 상품목록을 한 페이지 여는 것만으로 그 페이지 모든 상품의 옵션이 응답에 실렸다.
같은 패턴을 저장소 전역에서 찾아 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 가 되는 것까지 확인했다.
파일 목록과 선택한 파일의 설명에 `$t:…` / `{{ … }}` 가 그대로 보였다.
설명 원문은 소유 템플릿의 다국어 키인데 응답 조립 시점에는 요청 템플릿의
사전만 열려 있어 다른 템플릿의 레이아웃을 편집하면 키를 찾지 못했다.
번역 실패가 예외 없이 원문 노출로 이어져 화면에만 나타났다.
목록·상세가 같은 규칙을 쓰도록 표시 해석을 App\Support\LayoutDescription 으로
모으고, 컨트롤러가 소유 템플릿 사전을 주입한다. 표현식이 섞인 설명과 번역을
찾지 못한 경우는 파일 이름으로 대체한다.
목록만 고치면 상세 헤더에 그대로 남는다 — 브라우저 실측에서 목록 수정 후에도
헤더가 새던 것을 확인해 상세 경로(LayoutResource)까지 함께 처리했다.
레이아웃 코드 편집 화면이 목록을 부를 때 모든 레이아웃의 본문(content)까지 함께
내려받고 있었다. 레이아웃이 많은 템플릿에서는 목록을 여는 것만으로 수십 MB 가
브라우저에 쌓여 화면이 크게 느려졌고, 디버그 모드에서는 탭이 메모리 부족으로
종료됐다. 목록 화면은 본문을 쓰지 않고 선택한 파일 하나만 편집한다.
목록 전용 Resource 를 두어 이름·설명·크기·수정일만 싣고, 본문은 상세 조회에서만
읽는다. 크기는 목록에서 표시하므로 저장소가 길이만 계산해 돌려준다.
표현식 해석 로직이 엔진 전역에 복제되면서 복사본마다 기능 집합과 판정 기준이
갈라졌고, 같은 작성이 어느 렌더 경로를 타느냐에 따라 다른 결과를 냈다. 증상
대부분이 예외가 아니라 조용한 오답 또는 값 소실이라 재현 위치를 특정하기 어려웠다.
판정만 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" 로 보고되던 문제도 런너 사전 검증으로 막았다.
정렬 게이트 대조 하네스는 관계 정렬 변형만 쓰는 저장소를 탐지하지 못한 채 통과시키고 있었다.
탐지·제외·인자 파싱을 함께 고치고 단위 테스트를 신설했다.
계측 축이 "목록 SELECT" 하나였고 프로파일 12종이 커맨드 파일에 하드코딩돼
있어 확장이 자기 대상을 추가할 수 없었다. 확장이 설치·제거되는 설치본마다
"실제로 존재하는 대상"이 달라지므로, 대상을 소유자가 선언하고 코어가 수집하도록
뒤집었다.
- 선언 지점 두 곳, 스키마 하나: 코어 config/benchmark.php,
확장 getBenchmarkProfiles (모듈/플러그인). 수집은 method_exists 기반이라
인터페이스를 직접 구현한 서드파티 확장이 깨지지 않는다.
- 축 4종(목록/화면/쓰기/배치)을 실행기로 분리하고 컨테이너 태그로 주입 —
축이 늘 때 커맨드를 고치지 않는다.
- 화면 축은 HTTP 커널로 내부 요청을 처리한다. 라우트만 dispatch 하면 전역
미들웨어를 건너뛰어 화면과 다른 것을 재게 된다. 인증은 프로파일이 선언한
권한만 가진 임시 계정이 기본이며 --as 로 기존 계정을 지목할 수 있다.
- 화면/쓰기 축은 계측 계정 생성부터 처리까지 롤백되는 트랜잭션 안에서 실행해
운영 DB 에도 잔여 데이터를 남기지 않는다. 배치 축은 내부 커밋·락 보유 때문에
감싸지 않고 --allow-write 로만 통제한다.
- 잘못된 선언은 조용히 버리지 않고 사유를 경고로 남긴다. 버려진 선언이 곧
계측 사각이 되기 때문이다. 전부 건너뛴 실행은 종료 코드 1 로 끝난다.
- 리포트는 환경 정보를 함께 적어 저장소 밖(storage/app/benchmarks)에 남긴다.
측정값이 실행 머신에 종속되므로 저장소에 축적하면 비교 불가능한 수치가 섞인다.
작업 중 발견해 함께 고친 것:
- 이관 프로파일 3종의 soft_delete 선언이 실제 스키마와 어긋나 있었다
(users/product_inquiries/pages 에 deleted_at 없음).
- 주문 목록이 상태 미지정 시 임시 주문 상태를 NOT IN 으로 제외하는데 선언에
빠져 있었다. filters 가 등가 비교만 지원한 것이 근본 원인이라 연산자 형태를
받도록 넓히고, 미허용 연산자는 측정 전에 거부한다.
- 댓글 프로파일은 지배적 술어가 상관 서브쿼리라 선언형으로 재현할 수 없어
제거했다. 맨 테이블 스캔은 어느 화면도 내지 않는 수치가 된다.
g7:bench:pagination → g7:bench 로 개명(4축을 재므로). 이전 이름은 별칭으로 남겼다.
테스트 61건 통과, 라이브 18종 전건 측정.
목록이 뒤쪽 페이지로 갈수록 느려지는 원인은 건너뛰는 행의 넓은 컬럼까지 읽는 것이었다.
지연 조인(inner 는 키 컬럼만, outer 는 이번 페이지 행만 전체 컬럼)과 정렬 컬럼 해석을
코어 공통 트레이트로 두고 코어 8 + 확장 6 저장소에 적용했다.
감사 지적 처리:
- 그룹 쿼리의 총 건수 오산 — count 는 groupBy 를 벗기지 않아 첫 그룹의 행 수를
총 건수로 돌려준다. getCountForPagination 으로 교정.
- audit 전수 스캔이 버퍼 한계로 한 번도 동작하지 않던 결함과, 확장 경로를 놓치던
정규식 5건을 함께 고쳤다. 그동안 번들 확장의 위반이 보이지 않았다.
raw 20건을 빌더 표현으로 치환하고 하드코딩 테이블명 16건을 모델 유래로 바꿨다.
이 과정에서 마이페이지 「내가 댓글 단 글」 검색이 조인 별칭 충돌로 SQL 오류를 내던
결함이 해소됐다. 재발 차단용 룰 2종을 신설하고, 컬럼 프루닝 룰을 error 로 승격했다
(전역 위반 0건 — warning 으로 두면 "0 error" 판정을 통과하며 무음으로 재누적된다).
정렬 허용 목록은 FormRequest 게이트를 덮어야 한다. 좁으면 검증을 통과한 정렬이 422 도
로그도 없이 기본값으로 되돌아간다(스케줄 실행 이력의 종료시각·소요시간 정렬이 그랬다).
호출처를 스캔해 매핑 완전성까지 검사하는 계약 테스트로 전수 강제한다.
해외 배송지 결함도 함께 고쳤다 — 조회는 city/state/postal_code 로 내려주고 폼은
intl_* 로 제출하는 비대칭 때문에, 수정 시 입력칸이 비어 보이고 저장하면 기존 값이
유실됐다. 쿠폰 수정의 유효기간 미검증과 1인당 사용 제한 부분 수정 차단도 함께 수정.
Laravel chunk/each 는 OFFSET 기반이라, 콜백이 행의 필터 소속을 바꾸면
(필터 컬럼을 채우거나 매칭 행을 삭제하면) 다음 조회가 그만큼 앞으로 밀려
미처리 행을 조용히 건너뛴다. 250건/청크 100 이면 정확히 100건이 남는다.
코어·번들 확장 전수 조사로 확정한 6곳을 chunkById(키셋 순회)로 통일했다.
브라우저 검수(28축)에서 계획에 없던 결함 12건이 드러나 같은 사슬에서
수정했다. 이 중 layout_strategy=keep 무효화와 수정 감지 실패 시 "수정 없음"
오보는 사용자 작업 소실에 직결된다.
재발 방지로 audit 룰을 추가했고, 수정 6곳을 원복해 red 6건 / 복원 후 green
0건으로 판별력을 검증했다.
레이아웃 편집기 spec 중 저장(PUT)까지 수행하는 것들이 제품 화면에 편집 결과를
영속시켜, 실행할 때마다 개발 사이트에 노드가 누적됐다(home 에 빈 표 7개,
관리자 대시보드에 빈 차트 5개). spec 안에 "추가한 노드 삭제 후 재저장" 원복을
넣어도 소용이 없었다 — 원복 후에도 레이아웃이 오염 시점과 정확히 같은 크기로
되돌아왔다. 원복에 기대는 대신 저장 대상 자체를 E2E 전용 시드 화면으로 분리한다.
시드 화면은 활성 템플릿 디렉토리에만 설치되므로 배포 원본과 릴리스 산출물은
변하지 않는다. globalSetup 이 매 실행 fixture 원본으로 덮어써 회차 간 누적이
성립하지 않으므로 저장 spec 에 원복 절차가 필요 없다. 설치는 레이아웃 파일과
라우트, DB 행 세 가지를 함께 다루되 시드 행만 건드린다 — 전체 재동기화를 쓰면
편집기로 저장한(파일에 없는) 변경이 실행할 때마다 사라지기 때문이다.
전수 실행에서 드러난 기존 실패도 함께 처리했다. 35건 중 27건은 절전·부하
아티팩트였고 나머지는 원인을 확정해 고쳤다. 템플릿 식별자 자리에 모듈 id 를
넣어 라우트 트리가 비어 있었고, "캔버스 텍스트가 길어진다"는 대리 지표가 렌더
여부와 무관했으며(정상 렌더에도 짧아진다), 팔레트 삽입 위치가 잠금 서브트리
안이면 선택이 조상으로 밀려 노드를 지목할 수 없었고, 상태 개수 단언이 상태
추가로 낡았고, if 게이트상 그 탭에 등록되지 않는 소스를 refetch 하고 있었고,
무통신 구간이 오지 않는 관리자 화면에 networkidle 을 기다렸고, 테스트 종료 후
도착한 route 콜백이 워커를 중단시켰고, 같은 설정 화면에 저장하는 테스트들이
병렬로 충돌했다.
측정 방법에서 반복해 틀린 지점들은 E2E 가이드에 표로 남겼다 — 가림은 가시성
판정으로 잡히지 않고, 길이 증감은 렌더 신호가 아니며, 총 개수 단언은 스펙이
늘면 깨지고, networkidle 은 실시간 연결이 있는 화면에서 오지 않는다.
확장 E2E config 의 로케일 미고정을 코어 포함 7개에서 해소했다. 미지정 시 첫 페이지
로드의 Accept-Language 가 en-US 로 나가고 엔진이 그 값을 localStorage 에 굳혀,
인증 후에도 세션 전체가 영어로 렌더돼 한국어 문구를 단언하는 spec 이 전멸한다.
같은 함정을 정적으로 막는 룰을 함께 등록했다.
관리자 공용 탭·스위치는 ARIA 와 키보드 처리가 없어 마우스 없이는 조작할 수 없었고
보조기기에 목록·선택 상태가 전달되지 않았다. WAI-ARIA 규약대로 보완하고, role 변경에
맞춰 낡은 셀렉터를 갱신했다(admin_basic E2E 회귀 1건 포함).
nhnkcp 설정 화면은 저장 후 정규화 결과가 입력칸에 반영되지 않았다. 계측 결과
refetch 와 initLocal 은 정상 동작하며 _local 도 갱신되지만 사용자가 편집한 입력칸만
값을 유지한다. 저장 응답으로 폼을 명시 재바인딩하고 그 기전을 트러블슈팅에 남겼다.
safelist 생성기가 빌드마다 main.css 에 빈 줄을 누적해 매 빌드가 소스를 변경된 것으로
만들던 문제도 함께 고쳤다.
앞선 커밋이 CHANGELOG 에 주장한 "로딩 표시가 나오지 않던 문제 수정" 이 실제로
화면에 나타나는지 브라우저로 확정했다. 전달 경로부터 DOM 마운트·해제까지 측정했고,
지연·에러 응답에서도 영구 로딩이 남지 않음을 확인했다.
측정 과정에서 앞선 세션의 판단 하나가 틀렸음을 확인했다 — "오버레이 DOM 이 어느
화면에서도 관측되지 않는다" 는 관측 대상 노드를 잘못 잡은 것이었다. 스피너 방식
레이아웃은 다른 노드를 쓰며, 대조군에서도 안 보였던 이유가 이것이다.
미뤄 두었던 과제 4건을 함께 구현했다.
- 인증창을 그냥 닫고 이탈하면 그 인증 요청을 서버에도 종료로 표시한다.
이전에는 브라우저만 알고 있어 인증 이력에 "보낸 뒤 소식 없는" 항목이 남았다
- 플러그인 설정 화면이 저장된 비밀값을 다시 내려받던 것을 가려서 표시한다.
그 항목을 건드리지 않고 저장하면 저장된 값이 그대로 유지된다
- 언어팩 반영 누락 검사가 버전만 비교해 "버전 같고 내용 다름" 을 놓치던 사각을
콘텐츠 비교로 닫았다
- 확장이 검증 규칙을 추가할 때 쓰는 필터 이름 6개를 표준 형태로 통일했다.
예전 이름도 함께 발행하므로 기존 확장은 수정 없이 동작한다
정적 검사 룰 2종의 오탐·사각을 함께 고쳤고, 개명으로 검사 대상에 편입된 파일의
사전 결함 8건을 해소했다. 검수용 데이터는 전량 정리했다.
메일 인증 대신 휴대폰 본인확인을 쓸 수 있게 하는 플러그인을 추가한다.
가입·비밀번호 재설정·성인인증 등 코어 IDV 가 강제하는 모든 지점에서 동작하며,
테스트 모드로 계약 없이 전 흐름을 확인할 수 있다.
구현·검수 과정에서 드러난 저장소 전역 결함을 함께 처리했다.
- 외래키 컬럼의 한국어 comment 가 방출되지 않던 문제 (소스 38건 교정 +
기설치본 백필 업그레이드 스텝 7종). ->comment 를 ->constrained 뒤에
체인하면 예외 없이 통과하면서 comment 가 조용히 사라진다
- 로케일 전환 후 확장 액션 핸들러가 소실되던 문제 (플러그인 3종에
재등록 진입점 노출)
- 본인인증 상태 폴링 응답이 누적 시도 횟수를 함께 내보내던 문제
- 인증 안내 문구와 실제 진행 방식의 폭 경계 불일치 (768~1023px).
엔진과 같은 값을 같은 방법으로 읽도록 교정
- transition_overlay_target 이 replace 없이 무효이던 관리자 목록 40곳.
로딩 표시가 나오지 않아도 경고가 남지 않아 화면을 봐야만 알 수 있었다
두 인증 플러그인의 코어 최소 요구 버전을 7.0.6 으로 올린다. 운영 모드
자격증명 필수 검사가 7.0.6 의 설정 저장 검증 표면에 의존하며, 그보다 낮은
코어에서는 그 검사가 조용히 통과해 빈 자격증명으로 저장할 수 있었다.
재발 차단으로 정적 검사 룰 4종을 신설하고, 검사 도구가 미커밋 신규 확장을
"검사 파일 0" 으로 통과시키던 사각을 함께 막았다.
인터뷰에서 방향만 정하고 남겨 뒀던 후속 5건을 구현하고, 커밋 전 종합 점검에서
발견한 결함 2건을 함께 고쳤다.
기능 도입:
- 업로드 이미지 축소 — `upload.image_max_width` / `image_max_height` / `image_quality`
는 저장·검증만 되고 리사이즈 코드가 0건이었다. 비율 유지, 상한 이내면 재인코딩하지
않음(불필요한 화질 손실 방지), 한계 미설정이면 무동작(기존 사이트 이미지 보호).
PNG·GIF·WebP 투명도 보존. 실패해도 업로드는 계속되고 원본이 저장된다.
- 로그인 2단계 인증 — `security.two_factor_auth` 는 설정 항목만 있고 구현도 화면
컨트롤도 없었다. 코어 IDV 인프라를 재사용해 purpose `login` 을 추가하고, 비밀번호
확인 통과 후 challenge 만 발행한다(토큰 미발급 + logout). `POST /api/auth/login/two-factor`
가 코드를 확인해야 토큰이 나가며, 로그인과 같은 요청 제한을 적용한다. challenge 의
purpose 를 대조해 다른 흐름의 challenge 로 로그인하는 것을 막는다. 코드 발송이
실패하면 실패로 알린다 — 2단계를 건너뛰고 로그인시키면 통제가 조용히 열린다.
결함 수정:
- 게시글 수정 시 사용자 경로가 `attachment_ids` 를 Service 로 넘기지 않아, 검증과
권한은 통과하고 200 을 받는데 첨부만 조용히 연결되지 않았다. 관리자 경로는 넘기고
있어 같은 요청이 화면에 따라 다르게 동작했다.
- 설정 화면 라디오가 `pointer-events-none` + Label click 으로만 상태를 바꿔, 키보드
방향키(change) 조작 시 화면 표시와 저장될 값이 어긋났다. 동일 패턴 4개 레이아웃
12개 라디오에 change 액션을 부여하고 params 를 click 과 동일하게 고정한다.
- 주문완료 화면이 결제수단 확인보다 영수증 조회를 앞세워, PG 를 거치지 않는 주문에서도
매번 404 요청이 나갔다.
- 개별 게시판 폼의 숫자 항목에 경계값 바인딩이 없어 환경설정 화면과 안내가 달랐다.
커밋 전 점검에서 발견:
- 2단계 인증이 반쪽으로 들어가 있었다. enum 에 case 만 추가하고 코어 purpose
레지스트리와 ko/en 라벨을 빠뜨려, 목적 목록 API 에서 통째로 빠지고 `hasPurpose`
가 false 였으며 라벨 대신 i18n 키 원문이 반환됐다. 관리자가 이 목적의 메시지
템플릿·정책을 만들 수 없는 상태였다. 세 곳을 enum 기준으로 전수 대조하는 회귀
테스트를 함께 둔다.
- 업로드 이미지 축소가 적용되지 않은 8번째 서비스(레이아웃 편집기 첨부)가 있었다.
열거형 커버리지 테스트는 내 조사 결과를 green 으로 고정할 뿐 전수를 증명하지
못한다 — 같은 규약을 조건으로 표현한 audit 룰이 첫 실행에서 검출했다.
재발 방지:
- audit 룰 `upload-service-image-resize` (신규 업로드 서비스의 축소 누락 검출)
- 트러블슈팅 사례 20 (코어 설정이 일부 경로에만 적용되는 증상의 진단 절차)
- `sirsoft-ckeditor5` 의 코어 최소 요구 버전을 7.0.6 으로 상향 (신설 클래스 의존)
공개 이슈: gnuboard/g7
관리자 환경설정이 값을 저장은 하지만 실제 동작에는 닿지 않던 결함군을 정리했다.
설정 화면(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
템플릿 등록·수정·삭제·검수·상태변경을 제거하고 목록·상태·내용 조회 +
알림 연결만 남긴다. 등록·관리는 비즈뿌리오 콘솔로 위임 — 콘솔이 이미
이미지메이커·AI생성·캐러셀을 잘 제공하고, 카카오 규격이 자주 바뀌어 등록
폼을 유지하면 규격을 영구히 쫓아가야 하는 유지보수 부채가 크기 때문이다.
이 플러그인의 고유 가치는 카카오 템플릿 ↔ G7 알림 연결(binding)이다.
주요 변경:
- 라우트/컨트롤러/서비스에서 변경(manage) 메서드 전부 제거, 조회만 유지
- dev_sample 임시 개발코드 + FormRequest 3종 + 이미지 업로드 경로 제거
- 레이아웃: 등록 폼·상태변경 모달 제거, 상세는 내용만+[닫기], 목록 관리열은 [내용]만
- 환경설정 사용 전 준비 안내 + 목록 상단 콘솔 위임 안내(배지 의미색) 추가
- 안내 배너 중복 해소: 목록 조회 실패 배너에 ready 게이트를 걸어 설정 안내와 배타 표시
동반 변경(직전 세션 잔여, lang 얽힘으로 같은 커밋):
- 연동 행 SMS 대체발송 배지(ON/OFF solid 색) 추가
발송 결과(성공/실패·사유·잔액부족·SMS 대체발송)를 코어 "알림 발송 이력" 화면의 각 행에
표시한다. 코어 앱·코어 테이블 무수정 — 연결 표식은 bizppurio_dispatches.notification_log_id
에만 두고, 발송 사이클의 코어 after_log_sent/after_log_failed 훅으로 그 로그 id 를 연결한다.
- 결과 조회는 파라미터 없는 GET /dispatch-results/recent 로 최근 결과 맵을 받아 row.id 매칭
(목록 로드 순서에 무관 — 빈 배열 전송·타이밍으로 결과가 비던 문제 해소).
- 코어 이력 datagrid 에 결과 컬럼을 layout_extensions 로 주입하고, 행 토글에도 상세 결과를
얹는다. 메일·사이트내알림 탭에선 컬럼(헤더 포함)을 숨긴다. 다크모드 배지 색 보강.
- 브라우저 검수용 데모 시더(4상태·엣지 케이스 21건) + 전 계층 테스트(백엔드·Vitest 렌더)
+ 시나리오 매니페스트 + API 레퍼런스 동반.
Phase 6 재설계 — 코어 알림 설정 화면을 그대로 쓰고 알림톡 편집 시 전용 칸만 얹는다.
- 시딩 필터 훅(seed.*.notifications.translations) 편승으로 회원 알림에 sms·alimtalk
template 증강 (코어·모듈 무수정, cleanupStaleTemplates 에도 유지)
- 코어 편집 모달에 overlay 로 전용 칸 append(연결 템플릿·SMS 대체) — 변경 즉시 저장,
코어 저장 버튼과 무관 (코어 템플릿 무오염)
- 알림톡·SMS 탭 상태 배너(설정 미완료/테스트 모드)
- 1차 구현(우리 목록 통째 교체·게이트·우리 편집 모달) 폐기
전 계층 테스트 통과 (백엔드 170 / 프론트 63), audit 위반 0.
설정 페이지 알림톡 템플릿 탭에서 카카오 알림톡 템플릿을 등록·수정·삭제·검수·
상태변경한다. 진입은 코어 소유 설정 페이지 하나로 통일하고 getAdminMenus 는
폐기했다. 템플릿은 kapi 에 실시간 위임하며 DB 에 저장하지 않는다.
등록 폼은 계획서 §6-4 전체를 구현한다: 템플릿 코드(자동 생성/직접 입력),
카테고리 2단 드롭다운, 템플릿 유형(기본형/이미지형/강조표기형), 이미지형
파일 업로드, 본문 변수 클릭 삽입 + 알림별 변수 참고표, 선택 항목 6개
체크펼침(부가정보·미리보기·버튼·바로연결·대표링크·보안), 등록/등록 후
검수요청 2버튼.
문자·알림톡 발송 결과를 DB에 기록하고, 비즈뿌리오 webhook(URL PUSH)으로
도달 여부를 갱신한다. 결과코드를 사유+분류로 해석하고, 잔액 부족 시
관리자에게 쿨다운(기본 1시간) 내 1회만 알린다.
발송 결과를 확인하는 화면은 플러그인 독립 페이지 대신 코어 알림 이력에
얹는 방식(후속)으로 이관하기로 하여, 이번에 만든 독립 이력 화면과
화면 전용 백엔드는 제거했다. webhook 결과 저장·수신·해석 뼈대는 유지한다.
- 설정 화면을 공용 클래스·2단 그리드·상단 안내 패널·필드 인라인 에러로 통일
- 연동 환경(dev/live)을 검수 모드 Toggle(is_test_mode)로 전환, 독립 카드로
분리하고 운영 경고 박스를 API 연동 카드 상단에 배치
- 검수 모드를 끄면(운영) 발송 자격증명(아이디·비밀번호·발신번호)을
required 로 강제하는 조건부 검증 필터 훅 추가
- 리포트 수신 주소를 설정 화면에서 확인·복사 (플러그인 라우트가
config('app.url') 기준 절대 URL 반환, 코어 무수정)
- 명칭 "메시징→메시지 발송", 설명 "자동발송→발송", "(다우기술)" 제거
- 설정 저장 테스트가 실제 런타임 설정 파일을 오염시키던 결함 격리
신규 설치/복구 시 비-ko/en 번들 언어팩(ja 등)이 자동으로 채워지지 않아
런타임에 오류 없이 조용히 ko 로 폴백하고, 운영자가 이를 알아채기 어려웠다.
두 결손을 함께 해소한다.
프로비저닝(SSoT):
- language-pack:provision 멱등 커맨드 신설. supported_locales ∖ {ko,en} 의
미설치 번들 팩을 설치하고, 설치본 파일이 사라진 드리프트 팩(슬롯 점유로
getUninstalledBundledPacks 가 못 잡는)도 복구 경로로 겸한다. 재실행 시 0건 수렴.
발견성:
- CLI language-pack:list 를 Service::list 경유로 전환해 미설치(uninstalled)와
드리프트(active·파일 없음)를 함께 표면화.
- files_missing / bundled_source_available 파생 플래그를 Resource 에 노출.
- 관리자 언어팩 목록에 "파일 없음" 배지 + 원클릭 재설치(기존 install-bundled 모달
재사용). 재설치 경로만 auto_activate 로 바인딩해 복구 후 active 를 유지한다.
회귀 방지:
- 정적 룰 lang-pack-supported-locale-has-bundle(선언 로케일 ↔ 번들 소스 부재) 신설,
lang-pack-installed-version-drift 를 프로비저닝된 환경 한정으로 확장.
- 백엔드/레이아웃/E2E/시나리오 테스트 + 룰 fixture 추가.
부수 수정:
- 프로비저닝 작업 중 표면화된 ecommerce 언어팩 시더 테스트 격리결함 수정
(modules 등록 행을 매 테스트가 직접 보장 — 프로세스당 1회 시딩 전제 붕괴 대응).
일반 화면(React)과 봇 화면(PHP)은 같은 레이아웃을 각각 렌더하는데 봇
화면 쪽 지원 범위가 뒤처져, 확장이 교체한 본문이 통째로 빈 채 나갔다
(공개 이슈 ). 같은 결손 계열을 전수 조사해 함께 닫는다.
- 확장 주입 props 를 렌더 진입 시점에 해석해 자손 전체에 상속
→ 게시글·답글 원글·댓글·페이지·상품 설명 5개 지점 복구
- 사용자 작성 HTML 정화기 신설 — isHtml 판정에 따라 이스케이프/정화.
미정화 상태에서 봇 경로로 저장형 스크립트가 실제 실행됐다
- 노드 키 패리티: conditions / type:iterator / responsive 매칭 규칙 /
children 문자열 / text 우선순위 / $t:defer / 레이아웃 초기 상태
- 표현식: 단일 바인딩 판정 교체, ||·&& 를 값 반환(JS 시맨틱)으로,
비교식이 raw 경로에서 null 이 되던 문제
- 데이터소스: 엔드포인트 표현식 전면 해석 + 레이아웃 8곳 선언 보강
- 봇 페이지 캐시 1회 전량 무효화 업그레이드 스텝
지원 범위 SSoT 표를 문서에 고정하고, 한쪽만 고치는 것을 막는 검사 룰과
레이아웃 전수 순회 테스트로 잠갔다.
세 갈래의 결함을 한 브랜치에서 정리한다.
## 목록 컨텍스트 왕복 시 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/활성 디렉토리 동기 완료.
중첩 라우트의 상위 리소스 ID 를 조회 스코프에 반영하지 않아 A 의 경로로 B 의
하위 리소스를 읽거나 변경할 수 있었다. 방어 위치를 Repository where 절(SSoT)로
통일하고 컨트롤러 사후 비교에 의존하지 않게 했다. 같은 리소스의 형제 엔드포인트가
서로 다른 강도로 검증하던 부분도 맞췄다.
계층 리소스는 자기 자신·자손을 부모로 지정하는 순환을 검증 계층에서 차단하고,
검증을 우회하는 경로(시더/훅/오염 데이터)를 위해 path 재귀에 방문 ID 가드를 뒀다.
설정값이 정하는 깊이 상한을 Service 가 리터럴로 재클램프하던 부분을 제거해
게시판 설정이 실제로 적용되게 했다.
쓰기 경로는 validated 기준으로 좁혀 FormRequest 미정의 필드가 fillable 로
새지 않게 했다. 기존 오염 데이터는 업그레이드 스텝이 정리한다(삭제 없이 최상위로 이동).
재발 방지로 audit 룰 3종을 추가하고, 규정 문서와 트러블슈팅 사례를 함께 갱신했다.
공개 이슈: gnuboard/g7
오토로드 캐시(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 가 없어, 이후
모든 테스트 클래스가 중복 키로 깨졌다(단독 실행 시 미재현).
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 언어팩 동기.
관리자 카테고리 이미지 다운로드가 streamDownload 콜백 안에서
Storage::get 으로 파일 전체를 메모리에 적재한 뒤 echo 하던 안티패턴을,
공개 서빙과 동일한 CategoryImageService::download
(StorageInterface::response 스트리밍) 위임으로 교체한다.
서빙 계층은 이미 종단간 스트리밍이며 이 지점이 유일한 전체 적재 경로였다.
규정(storage-driver.md)에 서빙 안티패턴을 명시하고 audit coverage 에 등재한다.
계획서 위임 항목(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 번역 동기화
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)에 따라 마감 단계에서 일괄 수행한다.