- 열람 권한 없는 비밀글에 추천/비추천을 남길 수 있던 결함을 서버(ReactionService)와
화면(반응 영역 미노출) 양쪽에서 차단. 신고 기능과 동일한 권한 판정 기준 재사용.
- 반응 버튼 UX를 다듬어 활성 상태는 색상으로만 표시하고, 클릭 순간에만 살짝 눌리는
촉각 피드백(active:scale-95)을 추가.
- 이슈 명세 재검토 중 describe.skip 상태였던 반응 E2E spec 에서 결함 3건 발견·수정:
대상 게시판/게시글이 실제로 반응 불가능한 대상이었던 점, 3단계(해제) 검증이 값
우연 일치로 거짓 통과 가능했던 점을 고치고 실제 실행으로 통과 확인.
Chrome MCP 검수 중 발견한 결함 수정 및 요청 반영.
- 게시글 상세 응답의 reaction_counts 가 배열로 붕괴되어 개수가 0으로
표시되던 문제 수정 (PostResource → stdClass 로 JSON 객체 보장) + 회귀 테스트
- 반응 버튼을 수정·삭제 버튼과 같은 줄에 배치
- 반응 UX 개선: 클릭 즉시 하이라이트 반영 + 개수는 응답값으로 갱신(전체
재조회 제거) + 처리 중 로딩·중복클릭 차단 + 실패 시 롤백·안내
- board.md 상세 응답 필드 + basic CHANGELOG + 시나리오 매니페스트 갱신
5단계 마무리 — 기능 동작 확인 후 마감.
- tests/scenarios/board-reaction.yaml: 단일 case 축 18조합 + effects 22개,
각 테스트에 @scenario/@effects 마킹 (test-scenario-coverage 통과)
- board-reaction-flow.spec.ts: 등록→전환→해제 E2E (describe.skip, 토큰 환경 해제)
+ reactionToken fixture
- 부수 테스트: react 활동 로그 기록, 새 게시판 반응 기본값 시드(확정 13)
- board/basic CHANGELOG Added 에 반응 기능 기재 (working 버전 누적, +2 아님)
ja 언어팩 동기화는 ANTHROPIC_API_KEY( 환경) 필요 — 후속 진행.
4단계 화면 3면 + 백엔드 수용.
- 게시글 상세(show.json): 신고 버튼 위에 반응 영역 — use_reaction &&
유형 존재 시 노출, 유형 iteration(아이콘+라벨+개수, 0도 노출), 본인 글 비활성화,
같은 유형 재클릭=해제·다른 유형=전환, 비로그인 안내 후 로그인 유도
- 관리 설정 기본값 탭 + 개별 게시판 편집 폼: 반응 사용 토글 + 유형 체크박스
(reactionTypes data_source iteration, active_reaction_types code 토글)
- FormRequest(Store/Update Board, Store/BulkApply Settings)에 use_reaction·
active_reaction_types 화이트리스트/캐스트/시드 추가 (미추가 시 토글 미저장)
- BoardResource.icon_name(fa-* 토큰) — Icon 컴포넌트 접두사 중복 방지
- ko/en 다국어 + boards/settings API 문서 갱신
- Vitest 10건(템플릿 4 + 모듈 6) green, board 관리/설정 87건 회귀 green
ja 언어팩·시나리오 매니페스트·E2E·버전 bump·CHANGELOG 는 5단계.
공개 이슈(gnuboard/g7) 17건 전수 대조에서 "진짜 누락"으로 판정된 5건을 해소한다.
— 서버가 만들어 내보내는 상점 주소가 각 지점에서 `/shop` 리터럴이나
`"/{$routePath}/..."` 조립으로 흩어져 있었다. 기본값이 아닌 상점에서는 그 주소가
존재하지 않는 화면을 가리키는데, 서버는 문자열을 만들어 내려보냈을 뿐이라 예외도
404 로그도 남지 않는다 — 링크만 조용히 죽는다. `ShopPathResolver` 를 단일 해석
지점으로 두고 주문 완료 이동·비회원 조회 안내·알림 메일·통합검색·상품문의·SEO
캐시·사이트맵 전 지점을 경유시켰다. 같은 값에 종속된 PG 4종 리다이렉트는
`{shopBase}` 자리표시자로, 편집기 상태 scope 는 선택 세그먼트 토큰 `/*?`
(engine-v1.58.0)로 해소했다. 기설치본은 업그레이드 스텝이 백필한다.
M-02 — 답변 깊이 상한이 config 는 1~10 인데 컬럼 comment 만 1~5 로 남아,
comment 에서 자동 생성되는 계약 문서 4곳이 실제보다 좁은 상한을 공개하고 있었다.
L-01 — 관리자 게시글 폼에 첨부 안내가 아예 없었고, 대신 사장된 고정 문구
("최대 5개 파일, 각 10MB")가 남아 있었다. 게시판마다 다른 실제 설정값을 렌더한다.
— 목록 1페이지가 공지 전량을 상한 없이 읽던 것에 안전상한을 두고(구조 무변경,
limit 부가만), 조회수 정렬 목록 술어를 덮는 복합 색인을 신설했다.
브라우저 실측 검수에서 발견한 사전 결함 1건(사용자 게시판 목록이 URL 정렬
파라미터를 데이터소스로 전달하지 않던 문제)도 함께 고쳤다. 폴백을 빈 문자열로 둬
URL 에 정렬이 없을 때 게시판 설정을 덮어쓰지 않게 했다.
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 전환.
- 에스크로 표시가 영구히 렌더되지 않던 문제: is_escrow 가 DB·모델·부분취소 차단
로직에는 있으나 OrderPaymentResource 가 내보내지 않아, 이 값을 참조하는 주문
상세(관리자·구매자)의 에스크로 행이 항상 false 로 평가됐다. Resource 에 노출한다.
- 결제수단을 바꿔도 이전 수단의 입력이 주문에 실리던 문제: 결제수단 버튼이
_local.paymentMethod 만 바꾸고 슬롯이 소유한 확장 페이로드·환불계좌는 비우지
않았다. 확장 슬롯과 환불계좌 블록은 결제수단 조건부로 언마운트되지만 _local
값은 남으므로, 무통장에서 현금영수증·환불계좌를 입력한 뒤 카드로 전환하면 그
값이 주문 생성 요청에 그대로 실렸다.
프론트는 결제수단 전환 시 슬롯 소유 상태를 초기화하고, 서버는 환불계좌가 쓰이지
않는 결제수단이면 저장하지 않도록 게이팅한다(현금영수증은 이미 dbank 게이트 보유).
- Modal 이 받지 않는 size prop 을 쓰던 4곳을 width 로 교체(무시되어 의도한 폭이
나오지 않았다). 토스 에러 모달의 setState target 을 params 안으로 옮긴다.
- 레이아웃 편집기 팔레트에서 체크박스를 넣으면 라벨 없이 네모 칸만 놓이던 문제:
Checkbox 는 label prop 을 렌더하지 않으므로 Label 래핑 + Span 텍스트로 바꾸고,
Accordion 이 Label 을 받도록 중첩 스펙을 맞춘다.
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).
토스페이먼츠 플러그인을 프로덕션 수준으로 완성한다 (계획서 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(룰 미구현)라 규정이 있어도
검출되지 않았고, 실제로 이번 신설 테스트가 그 사각에 빠졌다. 룰을 구현해 봉인한다.
합의되지 않은 구조 변경을 되돌리고, 계획서 전수조사에서 드러난 결함을 처리했다.
쇼핑 화면 호출 통합(F4)은 API 쿼리 튜닝이 아니라 화면 구조 변경이라 원복했다.
레이아웃 9개를 develop 상태로 복원하고 전용 엔드포인트·컨트롤러·문서·테스트를 걷어냈다.
되돌리면 함께 사라지는 비-F4 수정 둘(페이저 has_more_pages 판정 3곳, Icon 크기 클래스
23줄)은 재적용했다. 성능 개선분은 전부 잔존한다.
통합검색 화면 상태를 전역 상태에서 URL 쿼리로 옮겼다. 종전에는 탭·필터·정렬·페이지가
주소에 없어 새로고침·뒤로가기에서 초기화되고 결과를 공유할 수도 없었다. 화면은 정상으로
보이고 콘솔 에러도 없어 코드만으로는 드러나지 않는 형태였다.
확장이 라우트를 추가해도 라우트 캐시가 걸린 사이트에서는 그 주소가 404 가 되던 구멍을
막았다. 훅 캐시와 달리 라우트 캐시에는 스캔 폴백이 없어 예외도 경고도 남지 않는다.
같은 문제를 이미 푼 ConfigCacheHelper 를 미러링해 갱신 지점을 한 곳으로 모았고,
코어 업데이트가 캐시를 비우기만 하고 되살리지 않던 문제도 함께 고쳤다.
상품·페이지 검색이 커서 판정에 페이지 번호를 넘기지 않아, 커서 없이 깊은 페이지를
지목한 딥링크가 조용히 1 페이지를 돌려주던 결함을 고쳤다. 호출 지점을 손으로 열거하는
대신 저장소를 훑는 패리티 테스트로 다음 도메인의 재발을 막는다.
환경설정 고급 탭의 목록 상한값이 저장되지 않던 결함을 고쳤다. 화면·검증·읽기는 모두
있었는데 저장 시 카테고리 분류표가 손으로 열거돼 있어 그 값만 버려지고 있었다. 분류표를
설정 정의에서 도출하도록 바꿔 다음 카테고리가 합류해도 같은 일이 생기지 않게 했다.
범위를 벗어난 값의 안내에 내부 식별자가 노출되던 것도 함께 정리했다.
언어팩 재설치가 활성 팩을 자기 자신과의 슬롯 충돌로 오인해 강등하던 회귀와, CLI 설치가
HTTP 권한 컨텍스트 없이 자동 활성화되는지를 고정하는 테스트를 함께 담았다.
저장소가 총 건수를 상한까지만 세게 되면서 마지막 페이지 번호가 없는 목록이
생겼는데, 화면은 그 값을 1 로 접거나 페이저 노출 조건으로만 쓰고 있었다. 앞은
"1페이지뿐" 으로 잘못 말하고 뒤는 페이저를 통째로 지워 뒤쪽 페이지로 갈 방법
자체를 없앤다. 둘 다 예외도 404 도 내지 않아 화면은 정상으로 보이고 기능만 없다.
Pagination 컴포넌트가 세 형태를 모두 이해하도록 넓혔다. 마지막 페이지를 알면
페이지 산술이 우선이고, 모를 때만 다음 페이지 존재 여부로 판정한다. 처음·마지막
버튼을 따로 켜고 끌 수 있게 나눠, 마지막 페이지를 모르는 목록에서는 그 버튼만
감춰지고 나머지 이동은 그대로 열려 있다. DataGrid 는 페이저를 그리는 두 경로
모두에 그 값을 전달한다 — 한쪽만 전달하면 그 화면만 첫 페이지에 갇힌다.
마이페이지 주문·마일리지·위시리스트·알림함, 게시판 목록, 상품 리뷰·문의, 내가 쓴
글, 관리자 설정 탭과 스케줄 이력의 소비 지점을 전건 옮겼다.
검색 화면은 총 건수를 정확히 세지 못한 경우 "N건 이상" 으로 표기하고 검색어를
좁히면 정확한 건수를 볼 수 있다고 안내한다. 탭 배지도 세지 못한 값을 정확한
숫자처럼 보여주지 않는다. 최신순은 첫 페이지부터 커서로 이동하며, 주소로 특정
페이지를 지목한 딥링크는 종전 동작을 유지한다.
쇼핑 첫 화면과 분류 화면을 단일 요청으로 재배선하고, 섹션 partial 이 부모가
선언한 이름을 읽도록 통일했다. 데이터소스 이름에 직결돼 있으면 다른 화면이
재사용할 때 폴백에 걸려 예외도 로그도 없이 섹션이 사라진다. 상품 상세의 "최근 본
상품" 이 계속 비어 있던 것은 존재하지 않는 라우트 파라미터를 읽고 있어서였다.
게시판 목록 세 스킨의 블라인드 아이콘 대비와 이미지 위 공통 색의 다크 모드 표기를
정리했다.
리베이스 결과를 브라우저로 검수하다 같은 화면에서 셋을 확인했다. 셋 다 응답에도
콘솔에도 신호가 없고 화면에서만 드러나는 종류다.
- 두 카드 어디에도 폭 규약이 없어 flex 가 max-content 비율로 폭을 나눴다. 모듈
파티션 레이아웃의 공백 없는 긴 파일명이 목록의 max-content 를 밀어 올려 편집기가
절반 이하로 눌렸다(실측 목록 814 / 편집기 473 → 384 / 903). 이름은 자르지 않고
접는다 — 잘라내면 어느 파티션인지 구분할 수 없다.
- 저장 뒤 상세만 다시 불러서, 방금 저장한 파일의 목록 행이 저장 전 설명·크기·수정일을
그대로 달고 있었다. 목록 응답이 본문을 싣지 않게 된 뒤로 재조회가 수십 KB 라
매 저장마다 부른다.
- 서버가 버전 목록에 기본 상한 100 을 도입했는데 이 화면에는 limit 도 '더 보기' 도
없어 상한 밖 버전이 도달 불가였다(실측 114건 중 14건). 페이징이 위지윅 편집기에만
들어가 상한 도입이 이 화면에서는 그대로 기능 축소가 됐다.
회귀는 레이아웃 구조(5건)와 브라우저(6건) 양쪽에서 잠근다. 브라우저 쪽은 수정을
되돌린 상태로 fail 을 먼저 확인했다.
관리자 상품목록을 한 페이지 여는 것만으로 그 페이지 모든 상품의 옵션이 응답에 실렸다.
같은 패턴을 저장소 전역에서 찾아 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 가 되는 것까지 확인했다.
마이페이지·비회원 주문 상세에서 상품별 배송정책명과 개별 배송비 줄이 아무 안내
없이 통째로 사라졌다. 주문에 보관하는 배송정책 스냅샷이 항목 목록과 배송지 메타를
한 배열에 섞어 담고 있어, PHP 배열이 non-sequential 이 되면서 json_encode 가 리스트가
아니라 객체로 직렬화했기 때문이다.
서버 소비자는 정수 키 판별로 관용 처리해 무사했고 배열을 전제한 화면만 죽었다.
한 컬럼에 두 의미를 담은 구조 자체를 items + address 로 분리했다.
이미 저장된 주문은 화면만 고쳐서는 낫지 않으므로 업그레이드 스텝으로 함께 정규화한다.
표현식 해석 로직이 엔진 전역에 복제되면서 복사본마다 기능 집합과 판정 기준이
갈라졌고, 같은 작성이 어느 렌더 경로를 타느냐에 따라 다른 결과를 냈다. 증상
대부분이 예외가 아니라 조용한 오답 또는 값 소실이라 재현 위치를 특정하기 어려웠다.
판정만 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)에서 설정이 빈 문자열로 남은 사이트의
주문서 입금기한 안내가 미치환 플레이스홀더를 노출하는 것을 실측해, 화면
폴백을 서버와 같은 규칙으로 맞췄다.
develop 리베이스 3커밋이 각자 정상인데 합쳐지면서 어긋난 지점들을 정리한다.
- 발송 이력 목록이 뒤 페이지로 갈수록 느려지고 같은 시각 발송분이 중복
노출되던 두 결함을 코어 지연 조인으로 함께 닫았다. 이 파일은 develop 쪽에서
들어와 492 브랜치에서는 검출될 수 없었다.
- 정렬 허용 컬럼·지연 조인 호출 계약 테스트가 순수 소스 스캔인데도 앱을 부팅해
케이스마다 DB 연결을 잡았다. 스캔 대상이 늘자 연결 상한(151)을 넘겨 실패했고,
앱 의존이 없으므로 부팅을 뺐다.
- 확장 업데이트 모달 테스트는 stub 이 모든 체크박스에 같은 식별자를 붙여,
검색 인덱스 재생성 체크박스가 추가되자 대상을 특정하지 못했다.
- 동의 이력 필터 어휘가 enum 전 케이스로 넓어진 것을 테스트 기대값에 반영하고,
이력 페이지네이션 버튼의 액션 구조를 나머지와 같은 형태로 통일했다.
- 관리자 확장 화면의 존재 확인 가드가 추가된 뒤 갱신되지 않은 mock 을 맞췄다.
다크 모드 대비가 빠진 버튼 둘을 저장소 관례에 맞추고, 사용자 템플릿에서
그 클래스가 실제 CSS 로 생성되도록 safelist 를 함께 채웠다.
목록 조회 성능 축(깊은 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" 로 보고되던 문제도 런너 사전 검증으로 막았다.
정렬 게이트 대조 하네스는 관계 정렬 변형만 쓰는 저장소를 탐지하지 못한 채 통과시키고 있었다.
탐지·제외·인자 파싱을 함께 고치고 단위 테스트를 신설했다.
목록이 뒤쪽 페이지로 갈수록 느려지는 원인은 건너뛰는 행의 넓은 컬럼까지 읽는 것이었다.
지연 조인(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건으로 판별력을 검증했다.
확장 E2E config 의 로케일 미고정을 코어 포함 7개에서 해소했다. 미지정 시 첫 페이지
로드의 Accept-Language 가 en-US 로 나가고 엔진이 그 값을 localStorage 에 굳혀,
인증 후에도 세션 전체가 영어로 렌더돼 한국어 문구를 단언하는 spec 이 전멸한다.
같은 함정을 정적으로 막는 룰을 함께 등록했다.
관리자 공용 탭·스위치는 ARIA 와 키보드 처리가 없어 마우스 없이는 조작할 수 없었고
보조기기에 목록·선택 상태가 전달되지 않았다. WAI-ARIA 규약대로 보완하고, role 변경에
맞춰 낡은 셀렉터를 갱신했다(admin_basic E2E 회귀 1건 포함).
nhnkcp 설정 화면은 저장 후 정규화 결과가 입력칸에 반영되지 않았다. 계측 결과
refetch 와 initLocal 은 정상 동작하며 _local 도 갱신되지만 사용자가 편집한 입력칸만
값을 유지한다. 저장 응답으로 폼을 명시 재바인딩하고 그 기전을 트러블슈팅에 남겼다.
safelist 생성기가 빌드마다 main.css 에 빈 줄을 누적해 매 빌드가 소스를 변경된 것으로
만들던 문제도 함께 고쳤다.
메일 인증 대신 휴대폰 본인확인을 쓸 수 있게 하는 플러그인을 추가한다.
가입·비밀번호 재설정·성인인증 등 코어 IDV 가 강제하는 모든 지점에서 동작하며,
테스트 모드로 계약 없이 전 흐름을 확인할 수 있다.
구현·검수 과정에서 드러난 저장소 전역 결함을 함께 처리했다.
- 외래키 컬럼의 한국어 comment 가 방출되지 않던 문제 (소스 38건 교정 +
기설치본 백필 업그레이드 스텝 7종). ->comment 를 ->constrained 뒤에
체인하면 예외 없이 통과하면서 comment 가 조용히 사라진다
- 로케일 전환 후 확장 액션 핸들러가 소실되던 문제 (플러그인 3종에
재등록 진입점 노출)
- 본인인증 상태 폴링 응답이 누적 시도 횟수를 함께 내보내던 문제
- 인증 안내 문구와 실제 진행 방식의 폭 경계 불일치 (768~1023px).
엔진과 같은 값을 같은 방법으로 읽도록 교정
- transition_overlay_target 이 replace 없이 무효이던 관리자 목록 40곳.
로딩 표시가 나오지 않아도 경고가 남지 않아 화면을 봐야만 알 수 있었다
두 인증 플러그인의 코어 최소 요구 버전을 7.0.6 으로 올린다. 운영 모드
자격증명 필수 검사가 7.0.6 의 설정 저장 검증 표면에 의존하며, 그보다 낮은
코어에서는 그 검사가 조용히 통과해 빈 자격증명으로 저장할 수 있었다.
재발 차단으로 정적 검사 룰 4종을 신설하고, 검사 도구가 미커밋 신규 확장을
"검사 파일 0" 으로 통과시키던 사각을 함께 막았다.
사용자 템플릿의 모달 닫기 버튼 접근성 라벨이 번역되지 않아 내부 식별자
(`common.close_modal`)가 그대로 노출됐다. 관리자 템플릿에는 키가 있는데
사용자 템플릿에만 빠져 있었고, 두 템플릿의 Modal 컴포넌트는 같은 키를 쓴다.
사용자 화면의 모든 모달이 영향을 받으며, 화면 낭독기 사용자는 버튼 용도 대신
내부 식별자를 듣게 된다. 눈으로 보는 검수로는 드러나지 않는 결함이라
접근성 트리를 읽는 과정에서 발견했다.
ko/en 키 추가 + 번들 일본어 언어팩 동기화(patch bump).
경위: 게시글 수정 화면에서 첨부를 빼면 실제로 삭제되는지 실측하던 중 발견했다.
첨부 삭제는 관리자·사용자 양쪽 모두 전용 즉시 삭제 경로로 이미 동작하며,
확인 모달이 "게시글 저장과 관계없이 되돌릴 수 없습니다" 로 그 의미를 안내한다.
인터뷰에서 방향만 정하고 남겨 뒀던 후속 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
관리자가 SMS·알림톡 채널을 꺼도 발송이 계속되던 문제 수정.
- 원인: 채널 토글 저장이 map-only 라 미등록 확장 채널(sms/alimtalk)의
on/off 가 저장되지 않았고, 백엔드는 미저장 채널을 활성으로 판정
- 프론트: 토글 저장을 upsert 로 전환(미저장 채널도 저장), 하단 채널 탭을
source 기준으로 필터(확장 채널은 미저장/OFF 시 숨김) — 코어·게시판·이커머스
- 백엔드: 플러그인이 core.notification.channel_enabled 훅을 구독해
확장 채널 미저장 시 OFF 로 게이트(코어 무수정, 코어 기본 채널은 하위호환 유지)
알림 설정 알림톡 탭에서 각 알림에 카카오 알림톡 템플릿을 연결하는 UI.
코어 편집 모달·저장 버튼은 건드리지 않는다(무오염).
- 각 알림 행 하단에 범용 확장 슬롯(notification_definition_row_footer) — 연결 상태 줄
+ [연결/변경] 버튼. 코어 편집 모달 내부 주입 불가(엔진 modals 미탐색) 회피.
- 우리 전용 연결 모달(modals 병합) — 승인 템플릿 드롭다운·SMS 대체 토글·변수 안내 +
[취소][저장]. 저장은 우리 API(코어 저장 무관). openModal 을 승인 조회보다 먼저 실행.
- 상태 배너(readiness 미충족/테스트 모드, 문제 있을 때만) + 알림톡 탭 안내 박스.
- SMS 대체발송 안내(실패 시 이 알림 본문이 SMS로 발송) + 다크 모드 solid 색.
- 카카오 API 422 는 errorHandling.suppress 로 조용히 처리(전 탭 에러 토스트 제거).
Vitest 17 통과, audit 위반 0, Chrome MCP 다크·저장 검수 통과.
신규 설치/복구 시 비-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/활성 디렉토리 동기 완료.
에셋 파일 서빙 방식은 언어·시간대와 무관한 인프라(서버 라우팅) 설정이라
지역화 카드에 묶이는 것이 의미상 어긋난다. 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 로 가드 유효성을 확인했다. 순서상 테스트가 읽는 소스만
바뀌었고 런타임 동작 변화는 없다.
계획서 §검증 후반부를 다시 대조해 미구현 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 기반이라 변경 없음.
결제 플러그인 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.
관리자 "전체 재생성"을 큐(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)에 따라 마감 단계에서 일괄 수행한다.
코어 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 누락 날짜 명시