- 버전 이력 변경량(+N/-N 줄) 계산이 변경 영역 (줄 수)² 크기의 LCS 표를 만들어
큰 공통 레이아웃의 첫 편집기 저장이 PHP 기본 메모리 한도(128M) 서버에서
500 으로 끝났다. 두 행 DP 로 길이만 구하도록 바꿔 메모리가 줄 수에 비례하고
표시 숫자는 종전과 같다(참조 구현 동치·메모리 상한 회귀 테스트)
- 확장 편집 모드 저장이 overlay 확장의 injections 를 비우던 결함: 서버가 주입
노드 출처 메타에 injection 순번을 싣고, 편집기는 그 순번(없으면 원본 노드 id)
으로 되돌리며, 되돌릴 수 없는 노드가 있으면 저장하지 않고 안내한다
- 서빙 캐시 키를 서버 현재 확장 캐시 버전으로만 조립해 저장·복원 두 번째부터
재로드·「최신 불러오기」가 옛 내용을 받던 결함 수정(요청 v 는 HTTP 캐시 우회용)
- 409 충돌 안내가 errors 아래의 버전을 읽지 못해 「최신 버전: -1」 로 표시되던
결함을 세 저장 경로 공용 판독으로 수정
- 버전 저장 시 저장자를 기록하고, 복원 시에도 잠금 번호를 올린다
- 회귀 테스트(PHPUnit·Vitest), 트러블슈팅 사례 32~34, 규정·API 문서, ja 언어팩,
편집기 번들 재빌드, 이력 문서 동반
공개 는 헤더 「로고 이미지」에 이미지를 지정하면 화면이 엑박이 되는 제보였다.
image 위젯은 배경용으로 설계되어 {url,size,repeat,position} 객체를 내보내는데, 값 슬롯이
하나뿐인 apply 경로(propValue/cssVar/단일 styleProp)가 그 객체를 그대로 props 에 기록해
소비 컴포넌트가 [object Object] 를 URL 로 받았다. 예외도 콘솔 오류도 서버 로그도 남지
않는다 — 깨진 이미지 요청은 SPA catch-all 때문에 404 조차 아니라 200(HTML)이고, 편집기
미리보기는 정상이라 조작 중에는 이상이 보이지 않는다.
방어선을 넷으로 세웠다. 쓰기 축약(공용 헬퍼 scalarizeImageValue 단일 지점, 게이트는 위젯
이름이며 값 형태 sniffing 이 아니다) · 읽기 역조립(표현식 문자열도 되감아 업로드 1클릭에
소실되지 않게 한다) · 런타임 방어(업그레이드 전 화면을 위해 템플릿 Img 가 url 을 해석하고
손상값이면 src 를 아예 붙이지 않는다) · 저장 데이터 백필(업그레이드 스텝). 런타임과 백필은
완전히 같은 엄격 판정식(키 집합 ⊆ 4키 AND url 보유)을 쓴다 — 엔진의 느슨한 판정식을 백필에
이식하면 레이아웃 전수에서 정상 props 2,219건을 파괴한다(실측).
전수조사에서 파생한 인접 결함 넷을 함께 고쳤다.
- number 위젯이 코어 레지스트리에 미등록이라 「탭 표시 게시판 수」 같은 컨트롤이
「지원하지 않는 컨트롤」로 폴백해 편집 자체가 불가했다. nodeKey apply 는 coreProps 가
선언만 하고 엔진 switch 에 case 가 없어 무음 no-op 이었다.
- 상속(base)·주입(extension) 노드 중 바인딩을 가진 것이 data_bound 로 분류돼 편집이
열려 있었는데, 저장 마스킹이 그 노드를 통째로 폐기하므로 편집분이 오류도 경고도 없이
사라졌다(저장은 200 이고 history 는 clear 돼 undo 도 불가). 출처 잠금이 항상 우선하도록
판정 순서를 통일하고, 단일 판정 헬퍼로 인라인 편집·복제·Delete·잘라내기·드래그 commit
까지 전 표면을 같은 기준으로 막았다.
- prop 자리의 표현식 값을 위젯이 해석하지 못해 빈 컨트롤로 보이고, 조작하는 순간 환경설정
과의 연결이 소리 없이 끊겼다. 판정·배지·잠금·해제·복구를 ControlRenderer 공용 게이트
한 곳으로 올려 신규 위젯에도 자동 적용되게 했다. 「직접 지정으로 바꾸기」에는 「되돌리기」를
동반해 편도가 되지 않게 한다.
- 편집기 모드에서 updateTemplateData 가 빈 레이아웃으로 같은 reactRoot 에 두 번째 커밋을
걸어 편집기 트리를 통째로 제거했다. renderTemplate 의 편집기 분기가 비동기라 부팅 중
setGlobalState 가 그 커밋 뒤에 도착할 때만 발현하는 경합이었다.
2단계 인증은 7.0.6 에서 서버측이 갖춰졌지만 인증번호를 입력할 화면이 어느 버전에도
없었다. 그래서 그 설정을 켠 사이트는 관리자를 포함한 전원이 로그인할 수 없었다.
원인은 `POST /api/auth/login` 이 조건에 따라 **다른 형태의 200** 을 돌려준다는 것이다.
평소에는 `{token, user}` 지만 2단계 인증이 켜져 있으면 `{two_factor_required,
challenge_id, ...}` 를 돌려준다. 프론트는 앞의 형태만 선언하고 `response.data.user.language`
를 바로 읽었으므로 그 자리에서 TypeError 가 났고, 영문 원문이 로그인 화면에 그대로 노출됐다.
서버는 정상 응답했으므로 서버 로그에는 아무 흔적도 남지 않는다.
이어서 `setToken(undefined)` 가 `localStorage` 에 문자열 `"undefined"` 를 남겼다.
이 값은 truthy 라 이후 모든 요청이 `Bearer undefined` 로 나가 401 이 되고, 사용자에게는
「세션이 만료되었습니다」로 보인다. 관리자 로그인은 한발 더 나가 `null->isAdmin` 으로
500 이 되어, 설정을 되돌릴 수단까지 함께 사라졌다.
## 구현
- 로그인 응답을 판별 유니온(`LoginResult`)으로 표현하고, 형태를 판별한 뒤에 읽는다.
`ApiClient.setToken` 은 비어 있지 않은 문자열만 저장한다.
- 사용자·관리자 로그인 화면에 인증번호 입력 단계를 추가했다. 같은 카드 안에서 넘어가며
「인증번호 다시 받기」와 「처음부터」를 제공한다. 관리자 판정은 코드 확인에 성공한 뒤에
수행하고, 거부할 때는 그 직전에 발급된 토큰을 회수한다.
- 재발송(`login/two-factor/resend`)은 기존 challenge 를 취소하고 새로 발행한다. 유효한
코드를 여러 개 살려 두면 대입 시도의 표적이 넓어진다.
- 인증번호를 보내지 못하면 401 이 아니라 503 으로 답한다. 자격 증명은 올바른데 401 로
뭉개면 사용자는 비밀번호를 의심하며 같은 시도를 반복하고, 운영자는 메일 설정이 깨진
사실을 알 방법이 없다.
- 공개 본인인증 경로(`identity/verify`·`cancel`)가 로그인 목적의 challenge 를 소진하지
못하도록 403 게이트를 세웠다. 소진되면 그 challenge 로 영영 로그인할 수 없다.
- 로그인 시도 제한 429 응답이 다국어 문구를 싣도록 했다(종전에는 프레임워크 기본 영문).
- 다국어 파라미터에서 파이프 표현식이 평가되지 않아 「유효시간 까지」처럼 값이 빠지던
문제를 함께 고쳤다. 같은 결함이 문의 목록 화면에도 있었다.
## 이번 점검에서 함께 고친 것
- 계정 잠금(423)·발송 실패(503) 응답이 사용자·관리자 컨트롤러에 동일하게 복제돼 있었고
그 주석 자신은 "단일 지점에서 만든다" 고 적혀 있었다. 페이로드에 필드가 하나 추가되면
한쪽만 따라가 같은 실패를 두 화면이 다르게 안내하게 된다 — 트레이트로 통합했다.
- 테스트가 개발자 자신의 사이트 설정을 읽고 있었다. 2단계 인증을 켜 둔 환경에서는 로그인
성공을 전제한 테스트가 503 으로 깨지는데 실패 메시지가 원인을 가리키지도 않는다.
같은 결함군을 위해 이미 존재하던 단일 지점에 그 축을 추가했다.
## 버전
코어 7.0.11 · sirsoft-basic 1.1.4 · sirsoft-admin_basic 1.0.9 ·
번들 일본어팩 3종 · 템플릿 엔진 engine-v1.65.0.
빈 번들 503 을 "에셋을 선언한 활성 확장이 있는데 결과가 비었다" 로 판정해,
스타일 규칙이 아직 없는 0바이트 CSS 만 선언된 기본 구성이 통째로 503 이 됐다.
file_get_contents 는 0바이트에서 false 가 아니라 '' 를 돌려주므로 읽기 실패
분기도 타지 않는다. 선언 축 게터는 전부 file_exists 게이트라 부재를 셀 수 없어,
존재 게이트가 없는 getDeclaredAssetAbsolutePaths 를 통로로 두고 503 의 근거를
소실 축(findMissingDeclaredAssets)으로 옮긴다. 존재하되 비었으면 빈 200 이다.
곁들여: 실패 배너 항목명을 내부 구분 키에서 사용자 어휘로(engine-v1.64.7),
봇 화면이 없는 템플릿 CSS 를 링크하지 않도록 실재 게이트, 매니페스트 자산
선언 드리프트 4건 정정.
공개 저장소 이슈 gnuboard/g7 (@bigmsg) 제보에서 출발한 작업이다.
브라우저가 화면을 그리려고 제3자 CDN 에 도달해야 하면, 그 도달 실패는 예외도 로그도
남기지 않고 화면 기능만 조용히 사라진다. 폐쇄망·방화벽·광고차단기에서 재현되는데
자체 서버 로그에는 흔적이 없어 운영자가 원인을 특정할 수 없다. 제보된 것은 편집기
하나였지만 같은 구조가 아이콘·글꼴·코드편집기·압축 라이브러리·설치 마법사·개발
대시보드에 똑같이 있었으므로, 번들 확장과 템플릿 전체를 자체 제공으로 옮겼다.
런타임에 외부로 나가는 것은 주소 검색 서비스 하나만 남았다.
자체 제공만으로는 부족하다 — 자기 서버에서 받는 파일도 실패할 수 있고, 종전에는 그
실패가 무음이었다. CSS 경로에 재시도 계층을 세우고(스크립트 경로와 동형), 서버가 HTML
에 직접 심는 externals 까지 실패를 붙잡아 안내 배너와 [다시 시도]로 표면화했다.
편집기·코드편집기는 확보 실패 시 평문 입력으로 내려앉되 저장 계약을 유지한다.
두 번째 축은 운영자가 CSS 를 덧붙일 자리가 없던 문제다(sir.kr 문의). 확장 디렉토리의
custom/ 을 운영자 소유로 정해, 확장 교체가 그 디렉토리만은 보존하게 했다. 출처에
의존하지 않는 서술자로 해석하므로 나중에 다른 출처가 붙어도 소비자는 바뀌지 않는다.
확장 자산과 같은 메커니즘으로 정적 게시되어 CSS 내부 상대 url 도 해석되고, 파일을
고치면 그 변경을 감지해 재게시까지 예약된다.
FTP 접근이 없는 운영자에게는 그 자리도 없는 것과 같으므로 레이아웃 편집기에서 직접
넣고 고칠 수 있게 했다. 모듈·플러그인·템플릿이 한 엔드포인트를 공유한다 — 타입별로
나누면 같은 검증이 세 벌로 갈리고 그중 약한 하나가 우회로가 된다. 여기서 올린
스크립트는 그 레이아웃 한 장이 아니라 사이트 전 화면에서 실행되므로 레이아웃 편집과
분리된 전용 권한으로 연다. 운영자 CSS 가 화면을 조작 불능으로 만들면 그것을 고칠
화면에도 같은 CSS 가 실려 스스로 갇히므로, 서버가 목록을 비우는 탈출구(?custom=off)를
함께 뒀다.
동봉 자산은 재생성 경로에 버전 대조 가드를 붙였다. 선언과 다른 버전을 버전 디렉토리에
써 넣는 조용한 거짓말은 배포된 뒤에는 드러나지 않는다.
관리자 상품목록을 한 페이지 여는 것만으로 그 페이지 모든 상품의 옵션이 응답에 실렸다.
같은 패턴을 저장소 전역에서 찾아 14개 목록 엔드포인트를 함께 정리했다.
근본 원인은 둘이다. Resource 가 whenLoaded 로 방어하는데 Repository 가 목록 쿼리에서
관계를 무조건 로드해 가드가 항상 참이 되는 가짜 가드, 그리고 toListArray 경량 표현을
정의해 두고도 컬렉션이 toArray 를 부르는 목록/상세 미분리다. 둘 다 응답만 보면
정상이라 오류도 경고도 없이 페이로드만 불어난다.
목록은 화면이 실제로 그리는 것만 싣는다. 개수·합계는 PHP 컬렉션 연산이 아니라 DB
집계로, 대표 1건이 필요한 곳은 관계 자체를 oldestOfMany 로 좁힌다. eager load 의
limit(1) 은 부모별이 아니라 배치 쿼리 전체에 걸려 첫 행만 값을 갖게 되므로 쓸 수 없다.
뺀 값에는 대체 경로를 먼저 만들었다. 상품 옵션은 행을 펼칠 때 배치로 불러오고(상품 수와
무관하게 쿼리 상수), 종전 동작이 필요한 호출자를 위해 ?with_options=1 등 opt-in 을 남겼다.
배송정책 국가설정과 리뷰 첨부 이미지는 소비처를 실측한 결과 화면이 실제로 그리고 있어
제거하지 않았다 — 그 소비 사실을 회귀 테스트로 고정했다.
재발 방지로 정적 검사 룰 4종과 규정 문서 항목을 함께 넣었다.
목록 조회 성능 축(깊은 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" 로 보고되던 문제도 런너 사전 검증으로 막았다.
정렬 게이트 대조 하네스는 관계 정렬 변형만 쓰는 저장소를 탐지하지 못한 채 통과시키고 있었다.
탐지·제외·인자 파싱을 함께 고치고 단위 테스트를 신설했다.