관리자 상품목록을 한 페이지 여는 것만으로 그 페이지 모든 상품의 옵션이 응답에 실렸다.
같은 패턴을 저장소 전역에서 찾아 14개 목록 엔드포인트를 함께 정리했다.
근본 원인은 둘이다. Resource 가 whenLoaded 로 방어하는데 Repository 가 목록 쿼리에서
관계를 무조건 로드해 가드가 항상 참이 되는 가짜 가드, 그리고 toListArray 경량 표현을
정의해 두고도 컬렉션이 toArray 를 부르는 목록/상세 미분리다. 둘 다 응답만 보면
정상이라 오류도 경고도 없이 페이로드만 불어난다.
목록은 화면이 실제로 그리는 것만 싣는다. 개수·합계는 PHP 컬렉션 연산이 아니라 DB
집계로, 대표 1건이 필요한 곳은 관계 자체를 oldestOfMany 로 좁힌다. eager load 의
limit(1) 은 부모별이 아니라 배치 쿼리 전체에 걸려 첫 행만 값을 갖게 되므로 쓸 수 없다.
뺀 값에는 대체 경로를 먼저 만들었다. 상품 옵션은 행을 펼칠 때 배치로 불러오고(상품 수와
무관하게 쿼리 상수), 종전 동작이 필요한 호출자를 위해 ?with_options=1 등 opt-in 을 남겼다.
배송정책 국가설정과 리뷰 첨부 이미지는 소비처를 실측한 결과 화면이 실제로 그리고 있어
제거하지 않았다 — 그 소비 사실을 회귀 테스트로 고정했다.
재발 방지로 정적 검사 룰 4종과 규정 문서 항목을 함께 넣었다.
비회원 주문 조회 상세는 회원 상세와 같은 화면 구성(partial)을 쓰는데
비회원 응답에만 배송정책 스냅샷이 없어 그 줄만 조용히 비어 있었다.
오류도 빈 값 표기도 없이 항목 자체가 사라지므로 응답을 열어 보기 전에는
드러나지 않는다.
스냅샷 전체를 그대로 내보내면 원가·계산 근거까지 비회원에게 노출되므로
ShippingPolicySnapshot::forDisplay 로 화면이 쓰는 정책명·개별 배송비만
추려 내보낸다. 내부 계산 정보는 종전대로 제공하지 않는다.
적립 포인트 절사가 코드에 1점 버림으로 고정돼 있어 운영자가 조정할 수 없었다.
언어/통화 설정의 환율 절사를 따라가는 방안을 먼저 검토했으나, 그쪽은 외화 환산
분기에서만 적용되어 기본 통화 상점에는 따라갈 값 자체가 없다. 적립은 기본 통화
원장에 정수로 확정 기록되는 값이라 성격이 다르므로 마일리지 통화별 규칙에 신설했다.
- earn_rounding_unit(1/10/100) + earn_rounding_method(floor/round/ceil)
- 기본값은 종전 하드코딩과 정확히 같은 금액을 산출 — 기존 쇼핑몰 적립액 무변화
- 주문 계산·옵션 분할·부분취소 안분 전 경로에 동일 기준 적용
- 주문 시점 값을 스냅샷으로 보관 — 이후 기준을 바꿔도 과거 주문이 소급 변경되지 않는다
- 설정 UI·검증·다국어(ko/en/ja)·API 문서 동반, 기존 사이트는 업그레이드 스텝이 명시 기입
함께 처리: 마일리지 내역 목록 테스트가 첫 컬럼 field 를 `no` 로 단언해 실패하던 것을
현재 레이아웃 값(`number`)에 맞춰 정정했다. 이 브랜치와 무관한 stale 케이스다.
마이페이지·비회원 주문 상세에서 상품별 배송정책명과 개별 배송비 줄이 아무 안내
없이 통째로 사라졌다. 주문에 보관하는 배송정책 스냅샷이 항목 목록과 배송지 메타를
한 배열에 섞어 담고 있어, 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" 로 보고되던 문제도 런너 사전 검증으로 막았다.
정렬 게이트 대조 하네스는 관계 정렬 변형만 쓰는 저장소를 탐지하지 못한 채 통과시키고 있었다.
탐지·제외·인자 파싱을 함께 고치고 단위 테스트를 신설했다.
계측 축이 "목록 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
PR 준비 비판적 재검토 중 발견한 잔여 항목 2건 정리.
- 게시판·이커머스 알림톡 연동 UI 테스트/레이아웃의 e2e:allow 주석·describe
블록에 남아있던 작업 중 이슈번호 제거 (audit 정규식은 형식만 검사해
이슈번호 잔존은 검출하지 못함)
- alimtalk-dispatch-binding.yaml 시나리오 매니페스트에 NotificationBindingService
저장 시점 승인 재검증(assertSendable) effect 2건 추가 — 코드/테스트는
이전 커밋에 있었으나 매니페스트만 누락
코어 알림 설정 화면에만 있던 비즈뿌리오 카카오 알림톡 템플릿 연결 UI(연결 버튼,
검수모드 배너, 안내박스)가 게시판·이커머스 알림 설정 화면에는 빠져 있었다.
Phase 6 재설계 시 "후속 작업"으로 남겨두고 실제 반영이 되지 않았던 부분이다.
- 게시판·이커머스 알림 정의 행에 코어와 동일한 notification_definition_row_footer
확장 슬롯을 추가하고, 게시판 레이아웃에는 코어/이커머스와 동일한 채널 콘텐츠
wrapper id를 신설해 구조를 맞췄다.
- 비즈뿌리오 플러그인에 게시판·이커머스 전용 overlay 확장 파일을 추가해 배너·
안내박스·연결모달·데이터소스를 각 레이아웃에 주입한다. 백엔드는 notification_type을
확장 구분 없는 문자열 키로 이미 3영역 공용 처리하고 있어 프론트 진입점만 보완했다.
- 회귀 테스트: 이커머스 화면에서 최초 구현 시 발견된 위치 버그(연결 버튼이 변수
뱃지 아래가 아니라 토글/편집 버튼 옆으로 붙던 문제)를 fail 재현 후 수정,
같은 회귀를 잡는 구조 검증 테스트를 게시판·이커머스 양쪽에 추가했다.
관리자가 SMS·알림톡 채널을 꺼도 발송이 계속되던 문제 수정.
- 원인: 채널 토글 저장이 map-only 라 미등록 확장 채널(sms/alimtalk)의
on/off 가 저장되지 않았고, 백엔드는 미저장 채널을 활성으로 판정
- 프론트: 토글 저장을 upsert 로 전환(미저장 채널도 저장), 하단 채널 탭을
source 기준으로 필터(확장 채널은 미저장/OFF 시 숨김) — 코어·게시판·이커머스
- 백엔드: 플러그인이 core.notification.channel_enabled 훅을 구독해
확장 채널 미저장 시 OFF 로 게이트(코어 무수정, 코어 기본 채널은 하위호환 유지)
신규 설치/복구 시 비-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회 시딩 전제 붕괴 대응).
세 갈래의 결함을 한 브랜치에서 정리한다.
## 목록 컨텍스트 왕복 시 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
여러 화면에서 조건부(FormRequest) 검증이 우회되거나 과도하게 제한되던 결함을 한 계열로 묶어 수정한다.
- 검증 규칙 확장 필터 이름 10개를 나머지 92개와 동일 규칙으로 통일
- 설명 길이를 바이트가 아닌 글자 수로 계산(한글 오검출 해소)
- 비활성 언어팩 번역 보존한 채 수정 허용
- 알림 템플릿 제목 nullable 존중, 미지원 발송 수단 저장 차단
- 일정/실행 이력 목록 조회 검증 정합
- 게시판 첨부 미사용 시 빈 확장자 통과, new_display_hours/최소 길이 0 허용, 일괄 적용 override_values 범위 검증(SSoT)
- 이커머스 클레임 사유 조회 검증, 용어 정정
- 원문 i18n 키 노출 다수 해소, 저장/삭제 실패 시 사유 화면 노출
- 아이콘 크기/다크모드 대비 회귀 수정
번들 확장(모듈3·플러그인6·템플릿2·언어팩5) 버전 new-bump 및 CHANGELOG 반영, ja 언어팩 동기.
담기/바로구매가 메인 옵션을 로케일 값 조합(option_values)으로 전송하고 서버가
문자열 동등비교로 역매칭하던 방식은, 클라·서버 로케일 불일치나 관리자의 옵션 값
텍스트 수정 시 매칭 실패를 유발했다. 클라이언트가 이미 보유한 옵션 ID(optionId)를
전송하고, 서버는 요청 상품의 옵션 집합 내에서 ID로만 조회하도록 전환한다.
추가옵션(value_id/custom_text)은 이미 ID 기반이라 변경 없음.
관리자 카테고리 이미지 다운로드가 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 를 정정.
사이트맵을 리소스 단위로 영속화(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) 표면을 동반했다.
대용량 사이트에서 사이트맵 생성이 메모리 부족으로 반복 실패하던 문제(공개 )의
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 누락 날짜 명시
확장이 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 인프라
미출시 예약 작업 기능에서 스케줄 권한만 위임받은 계정이 Shell/Artisan
명령으로 임의 코드를 실행할 수 있던 문제를 선제적으로 차단(KVE-2026-1567).
Shell 은 기본 차단 + 실행 파일 화이트리스트 + 셸 미경유 인자배열 실행,
Artisan 은 차단목록. 저장 시점(FormRequest)과 실행 시점(Service) 이중 검증.
아울러 공개 release 보다 앞선 미출시 버전들의 CHANGELOG 출시 날짜를
2026-07-14 로 통일.
## 소스맵 노출 차단 (공개 이슈 )
배포본에 .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 는 미출시 누적.
서버가 외부 입력으로 목적지가 제어되는 outbound HTTP 요청을 보내는 지점을
전수 조사해, 내부망(사설 IP·루프백·링크로컬·메타데이터)과 host 위조를 차단한다.
KVE-2026-1546(이니시스 인증 URL 접두사 매칭 우회)과 동일 클래스의
지점(언어팩 URL 설치, 스케줄 URL 호출, 배송비 계산 API, GitHub URL 판정)을
코어 공용 유틸 App\Support\OutboundUrlValidator 로 수렴시켰다.
- 접두사 매칭을 host 완전일치로 교체 — userinfo(@) 위장과 접미사 확장 도메인
우회 벡터 차단.
- 사내 서버 호출이 정당한 관리자 지점(스케줄·외부 API)은 신규 설정
security.allow_internal_outbound_urls(기본 off)로 옵트인.
- 언어팩 URL 설치는 원격 코드 다운로드라 옵트인과 무관하게 내부 주소 항상 차단.
- 웹소켓 연결 테스트는 localhost·사설 IP 가 정상 구성이므로 구조적 위조만 차단.
각 지점에 red 재현 → 수정 → green 회귀 테스트 + 실브라우저 E2E 로 검증.
수기 API 문서가 자동 재생성으로 소실되는 사고를 막는 PreToolUse 훅을 함께 추가한다.
이커머스 알림 정의 화면에서 order_pending_deposit·order_delivered 두 알림이
라벨 대신 raw 다국어 키로 표시되던 문제를 수정. settings.json 3로케일에 라벨
추가 + 렌더 회귀 테스트 + 브라우저 실측.
vendor-bundle 시스템 결함 5종 해결:
- (A/B) manifest 해시 기준을 활성이 아닌 출력(_bundled) 디렉토리로 통일해
확장 업데이트가 composer_json_sha_mismatch 순환에 빠지던 문제 해소
- (C) 코어 빌드 시 composer autoload dump 의 async 손자 프로세스가 부모
파이프 핸들을 상속·점유해 무한 대기하던 교착을, stdout/stderr 를 파이프
아닌 파일 descriptor 로 받아 원천 차단 (파이프=행/파일=완주 A/B 실측으로 확정)
- (D) 빌드 실패 시 원본 번들 유실 방지 — 임시파일 성공 후 원자적 교체
- (E) Windows .bat 실행 시 bypass_shell 통일
코어 7.0.4 / 이커머스·언어팩 1.0.3 bump.
7.0.3 로 업데이트하는 서버에서 module:update sirsoft-ecommerce 가
"번들 파일의 무결성 검증에 실패했습니다" 로 중단됐다.
앞선 커밋이 composer.json 의 version 을 1.0.1 → 1.0.2 로 올리면서
vendor-bundle:build 재실행을 빠뜨려, vendor-bundle.json 이 옛 해시
(8a46fadc…)를 그대로 들고 있었다. 실제 파일은 aa8f7406… 이다.
이 상태가 v7.0.3 태그에 그대로 실렸다.
로컬에서 재현되지 않은 것은 ModuleManager 의 isComposerUnchanged 가
staging 과 active 의 composer.json 을 비교해 같으면 vendor 설치를
통째로 건너뛰기 때문이다. 이미 1.0.2 가 깔린 환경은 검사 자체를 타지
않는다. 1.0.1 에서 올라오는 서버만 설치 경로로 들어가 검증에 걸린다.
재빌드로 manifest 해시를 실제 파일과 맞추고 g7_version 도 7.0.3 으로
갱신했다. zip 내용물은 432개 파일 중 vendor/composer/installed.php 의
자기 버전 표기(1.0.1 → 1.0.2) 한 곳만 달라지고 htmlpurifier 를 포함한
나머지 431개는 바이트 단위로 동일하다. 의존성 변화는 없다.
vendor-bundle:verify-all 로 코어와 전 번들 확장이 통과함을 확인했고,
서버와 같은 경로(--vendor-mode=bundled)로 module:update 를 실제 실행해
성공을 확인했다.
공개 release 빌드의 파일 본문 누출 검사에서 검출된 항목을 정리한다.
api-documentation.md 는 릴리즈에 포함되지 않는 내부 지침 파일명을 참조하고
있었다. 공개본 독자에게는 존재하지 않는 파일을 가리키므로 공개 진입점
문서(AGENTS.md)로 교체했다.
이커머스 배송국가 테스트 2종의 주석에서 내부 역할 호칭을 제거했다. 판단
근거 서술은 그대로 두고 호칭만 걷어냈으므로 어서션·실행 경로는 불변이다.
검증: check-public-release-leakage error 0 / warn 0,
PruneEmptyShippingCountryNameLocalesTest 10/10 통과.
$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건을 확인했다.
상품 목록의 숫자 6개 열 중 `구매수량`·`적립예정` 만 가운데 정렬이라 나머지 네 열과
세로 기준선이 어긋나 보였다. 두 열은 값 아래 배지를 쌓기 위해 `flex flex-col` 을
쓰는데 교차축 정렬이 `items-center` 로 남아 있던 것이 원인. `items-end` 로 바꿔
배지 세로 배치를 유지한 채 `text-right` 열과 같은 우측 기준선에 맞췄다.
공유 컴포넌트인 DataGrid 는 8개 이상 레이아웃이 사용하므로 손대지 않았다.
배포 후 실제 DOM 실측으로 확인: Quantity·Points 는 alignItems=flex-end,
Price·Discount·Actual Price·Subtotal 은 textAlign=right, 비숫자 열(상품정보·상태·
배송)은 정렬 대상에서 제외.
회귀 spec 추가 — `items-center` 는 text-align 이 아니라 align-items 로 정렬하므로
textAlign 만 단언하면 두 셀을 영영 못 잡는다. 콘텐츠의 우측 여백을 재고, 절대
임계값 대신 같은 행 안에서 열끼리 여백이 서로 같은지를 단언한다. 헤더 텍스트로
열을 찾지 않는다(E2E 세션 로케일이 en).
함께 수정 — 모듈 E2E 스위트가 수집 단계에서 전체 실패하고 있었다. 모듈 config 는
--config 로만 실행되어 여태 아무도 돌린 적이 없었다.
- 미정의 fixture 3종 추가(userToken/customerToken/ordersReadToken). Playwright 는
describe.skip 블록도 수집 시점에 fixture 파라미터를 해석한다.
- mp06 spec 주석에서 `OrderCalculation*/` 의 `*/` 가 블록 주석을 조기 종료시켜
SyntaxError 가 나던 문제 수정.
검증: 모듈 E2E 2 passed/63 skipped/0 failed, Vitest 2382 passed/5 failed
(clean HEAD 와 동일 — 선행 결함, 무회귀), audit 0 error.
모바일 헤더 우측 그룹이 420px 를 요구해 폰 뷰포트에서 통화 선택기가 잘렸다.
언어·통화·배송국가를 드로어로 옮겨 가로 칩으로 통일했으나, 이번엔 드로어가
세로로 길어져 정작 메뉴가 스크롤 밖으로 밀렸다.
언어와 통화·배송국가를 각각 독립 아코디언(기본 접힘)으로 바꾸고, 접힘 상태에서도
트리거에 현재 선택값을 요약 표기해 펼치지 않고 확인할 수 있게 했다. 언어는 템플릿이,
통화·배송국가는 이커머스 주입 조각이 각자 상태를 소유해 서로 간섭하지 않는다.
통화 패널은 새 플래그를 만들지 않고 데스크톱 드롭다운의 기존 토글을 재사용한다 —
portable 오버라이드에서 if 를 생략하면 엔진이 base if 를 상속하므로 자동으로 기본
접힘이 된다. 이 과정에서 모바일로 펼칠 때 전체 화면 배경 오버레이가 드로어 클릭을
가로채는 결함을 함께 차단했다.
그 밖에 모달 좌우 여백, 주문 취소·일괄변경 팝업의 세로 스택 전환, 은행 관리 입력칸
축소, 쇼핑몰 환경설정·쿠폰·엑셀 다운로드 화면의 가로 넘침을 함께 해소했다.
390px 실측 결과 26개 라우트 중 24개에서 문서가 뷰포트를 11px 넘겼고,
320px 에서는 햄버거 버튼이 화면 밖으로 밀려 내비게이션 자체가 불가능했다.
원인은 예외 없이 헤더 우측의 언어(71px)+통화(91px) 선택기였다.
이를 모바일 드로어로 옮기고, 좁은 화면에서는 세로 목록 대신 가로 칩으로
나열하도록 했다. 통화·배송국가는 이커머스 모듈이 슬롯에 주입하는 조각이므로
모듈 쪽에도 responsive.portable 오버라이드를 넣었다. 템플릿의 드로어 자리는
overflow-y-auto 라 옛 absolute 드롭다운이 잘리므로, 템플릿의 이커머스 모듈
의존 하한을 >=1.0.2 로 올렸다.
게시글 상단 네비는 성격이 다른 회귀였다. 게시판 이름이 길수록 flex 형제인
목록·이전글·다음글 버튼이 좁아지는데(87→73→62px), document.scrollWidth 는
내내 뷰포트와 같아 오버플로 검사로는 잡히지 않았다. 넘치는 게 아니라 줄어드는
회귀다. shrink-0 으로 버튼 폭을 고정하고, 모바일에서만 게시판 이름을 자기 줄로
올렸다. 버튼 3개 고정폭 261px + 21자 제목 323px = 584px < 태블릿 콘텐츠 735px
이므로 portable 이 아니라 mobile 스코프로 한정했다.
커밋 전 점검에서 audit 결함 두 건이 드러나 함께 고쳤다.
첫째, layout-* 룰 28개 중 27개가 appliesTo 에 확장 주입 조각 경로를 빠뜨려
조각 32개가 정적 검사에서 통째로 빠져 있었다. 실제 위반 60건이 은닉돼 있었고
그중 45건은 error 다. 다만 glob 만 넓히면 layout-grammar 의 root 화이트리스트가
조각의 정당한 키(target_layout/injections/extension_point/mode/priority)를 전부
오탐해 90건의 error 를 새로 만든다. 조각은 레이아웃이 아니라 별도 스키마이므로
extension-fragment 스키마를 신설해 오탐을 없애는 동시에 조각 고유의 결함(대상 키
누락으로 인한 silent drop, target_layout ↔ extension_point 동시 정의, 무동작 조각)을
새로 검출하게 했다.
둘째, Icon 의 w-N h-N 을 size prop 으로 바꾸라는 규정이 틀렸다. Icon 은 <i> 글리프라
박스 크기가 곧 font-size 인데, size prop 은 Font Awesome fa-* 로 매핑되어 상대값
0.875em + line-height 붕괴를 동반한다. 실측하면 w-4 h-4(16×16)가 fa-sm 에서
12.25×0.88 이 된다. 등가는 text-base 하나뿐이다. 앞선 작업에서 size:"sm" 으로
치환했던 2건을 되돌리고, 규정과 룰 메시지를 실측 등가표로 교체했다.
확장 API 문서가 인덱스·목차 어디에도 노출되지 않아 처음 온 개발자/AI 가
발견할 수 없던 간극을 해소했다. 코어에 확장명을 하드코딩하지 않고(동적 로딩 원칙),
파일 시스템 패턴 스캔 + 확장 소유 목차 규약으로 발견성을 확보한다.
- ApiDocScaffolder::readmeIndex — 각 대상의 docs/api/README.md 목차(도메인 파일·
엔드포인트 수)를 @generated 블록으로 생성(멱등, 사람 개요 보존). 커맨드가 대상별
README 를 방출(readmeFile 경로 헬퍼). 코어 + 9 확장 = 10개 목차 생성.
- generate-docs-index.cjs scanExtensionApiReadmes — {modules,plugins}/_bundled/*/
docs/api/README.md 를 패턴 스캔(확장명 하드코딩 0)해 ·AGENTS.md 에
"확장 API 레퍼런스" 표로 자동 편입. 확장 추가/삭제 시 재생성만으로 반영.
- api-documentation.md 에 README 목차 규약 + 발견 경로 명문화. coverage.json 에
api-doc-readme-index(manual-only) 등록. 루트 README API 레퍼런스 링크 갱신.
테스트: readmeIndex 목차 생성·멱등 2건 + 스캐너 편입·하드코딩부재 2건(node --test).
에서 이커머스 모듈 버전을 1.0.0 → 1.0.1 로 올리며 composer.json version
필드를 변경했으나 vendor-bundle 을 재생성하지 않아, 번들 기록 해시와 실제
composer.json SHA 가 불일치. VendorIntegrityChecker 가 module:update 를 차단.
module:vendor-bundle 로 재생성해 해시를 재동기화하고 무결성 검증을 복구.