공개 는 헤더 「로고 이미지」에 이미지를 지정하면 화면이 엑박이 되는 제보였다.
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 가 그 커밋 뒤에 도착할 때만 발현하는 경합이었다.
53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
import React from 'react';
|
|
|
|
export interface ImgProps extends React.ImgHTMLAttributes<HTMLImageElement> {}
|
|
|
|
/**
|
|
* `image` 위젯 값 객체의 키 집합 — 레이아웃 편집기가 만들던 `{url,size,repeat,position}`.
|
|
*
|
|
* 편집기는 이제 단일 값 슬롯에 url 문자열만 기록하고(코어 변환 계층), 이미 잘못 저장된
|
|
* 레이아웃은 업그레이드 스텝이 정정한다. 이 상수는 **업그레이드 전 화면**을 위한
|
|
* 런타임 방어이며, 판정 강도를 백필과 **완전히 같게** 유지한다 — 두 방어선의 기준이
|
|
* 어긋나면 한쪽만 통과하는 값이 생긴다.
|
|
*/
|
|
const IMAGE_VALUE_KEYS = ['url', 'size', 'repeat', 'position'] as const;
|
|
|
|
/**
|
|
* `src` 로 들어온 값에서 실제 이미지 URL 을 해석한다.
|
|
*
|
|
* 문자열이면 그대로(회귀 0). 이미지 값 객체(키 집합 ⊆ 4키 **AND** `url` 보유)면 그 url.
|
|
* 그 외(배열·숫자·불리언·4키 밖 키를 가진 객체·비문자열 url)는 `undefined` 를 돌려주어
|
|
* `src` 속성을 **아예 붙이지 않는다** — `src="[object Object]"`(엑박)도
|
|
* `src=""`(현재 문서 재요청)도 만들지 않는다.
|
|
*
|
|
* `ImgProps` 를 넓히지 않는다(`src?: string` 계약 유지) — export 하면 그 자체가 공개
|
|
* 계약이 되어 느슨한 타입이 다시 새어 나간다.
|
|
*/
|
|
function resolveImageSrc(src: unknown): string | undefined {
|
|
if (typeof src === 'string') return src === '' ? undefined : src;
|
|
if (!src || typeof src !== 'object' || Array.isArray(src)) return undefined;
|
|
const keys = Object.keys(src as Record<string, unknown>);
|
|
if (!keys.every((k) => (IMAGE_VALUE_KEYS as readonly string[]).includes(k))) return undefined;
|
|
const url = (src as { url?: unknown }).url;
|
|
return typeof url === 'string' && url !== '' ? url : undefined;
|
|
}
|
|
|
|
/**
|
|
* 기본 이미지 컴포넌트
|
|
*/
|
|
export const Img: React.FC<ImgProps> = ({
|
|
className = '',
|
|
alt = '',
|
|
src,
|
|
...props
|
|
}) => {
|
|
return (
|
|
<img
|
|
className={className}
|
|
alt={alt}
|
|
src={resolveImageSrc(src as unknown)}
|
|
{...props}
|
|
/>
|
|
);
|
|
};
|