본문으로 건너뛰기

커스텀 이미지 업로드

customImageUploadOptions를 사용하여 이미지 업로드 로직을 완전히 커스터마이징할 수 있습니다. 서버 응답 형식이 { "link": "URL" }이 아니거나, 복잡한 업로드 로직이 필요한 경우에 사용합니다.

실제 동작 예제

💡 실제 파일로 커스텀 업로드 로직을 테스트해보세요! (로컬 처리)
커스텀 설정:
로딩 중...

기본 사용법

// param 설명
// editor: FroalaEditor 인스턴스
// fileList: 업로드할 파일 목록 ( file[] )
// insertImage: 이미지 삽입 함수 (참고: https://froala.com/wysiwyg-editor/docs/methods/#image-insert-link-sanitize-data-existing_image-response)

const customUploadOptions = {
imageBeforeUpload: async (editor, fileList, insertImage) => {
// 커스텀 업로드 로직
const imageUrl = await uploadToServer(fileList[0]); // 서버에 이미지 업로드
insertImage(imageUrl, false, {}, null, {}); // 이미지 삽입
},
};

<FroalaEditor
customImageUploadOptions={customUploadOptions}
// ... 기타 속성
/>;

언제 커스텀 업로드를 사용해야 할까?

🔄 서버 응답 형식이 다른 경우

// 서버가 이렇게 응답하는 경우 ( 응답 속성 중 url -> link 가 아닌 경우 )
{
"success": true,
"data": {
"imageUrl": "https://example.com/image.jpg",
"imageId": "12345"
}
}

// customImageUploadOptions로 처리
const customOptions = {
imageBeforeUpload: async (editor, fileList, insertImage) => {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();

// 응답에서 이미지 URL 추출
insertImage(result.data.imageUrl, false, {}, null, {});
}
};

📝 파일명 자동 변경이 필요한 경우

function AutoRenameUpload() {
const [content, setContent] = useState('');

const autoRenameOptions = {
imageBeforeUpload: async (editor, fileList, insertImage) => {
// 1. 파일명을 날짜_랜덤문자로 변경
const timestamp = new Date().toISOString().slice(0, 10);
const randomId = Math.random().toString(36).substring(2, 8);
const extension = file.name.split('.').pop();
const newFileName = `image_${timestamp}_${randomId}.${extension}`;

// 2. 새로운 파일명으로 업로드
const formData = new FormData();
formData.append('file', file, newFileName);

const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
});

const result = await response.json();
insertImage(result.url, false, {}, null, {});
},
};

return (
<FroalaEditor value={content} onChange={setContent} customImageUploadOptions={autoRenameOptions} height={300} />
);
}

🔄 여러 서버 중 선택이 필요한 경우

function MultiServerUpload() {
const [content, setContent] = useState('');

const multiServerOptions = {
imageBeforeUpload: async (editor, fileList, insertImage) => {
// 파일 크기에 따라 서버 선택
const uploadServer =
file.size > 1024 * 1024 * 2
? '/api/upload/large' // 2MB 이상은 대용량 서버
: '/api/upload/small'; // 2MB 미만은 일반 서버

const formData = new FormData();
formData.append('file', file);
formData.append('server_type', file.size > 1024 * 1024 * 2 ? 'large' : 'small');

const response = await fetch(uploadServer, {
method: 'POST',
body: formData,
});

const result = await response.json();
insertImage(result.image_url, false, {}, null, {});
},
};

return (
<FroalaEditor value={content} onChange={setContent} customImageUploadOptions={multiServerOptions} height={300} />
);
}

고급 커스텀 업로드 예제

진행률 표시가 있는 업로드

function ProgressiveUpload() {
const [content, setContent] = useState('');
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);

const progressUploadOptions = {
imageBeforeUpload: async (editor, fileList, insertImage) => {
setIsUploading(true);
setUploadProgress(0);

return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('file', file);

const xhr = new XMLHttpRequest();

// 업로드 진행률 추적
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
setUploadProgress(Math.round(percentComplete));
}
});

xhr.addEventListener('load', () => {
setIsUploading(false);
setUploadProgress(0);

if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
insertImage(response.url, false, {}, null, {});
} else {
reject(new Error('업로드 실패'));
}
});

xhr.addEventListener('error', () => {
setIsUploading(false);
setUploadProgress(0);
reject(new Error('네트워크 오류'));
});

xhr.open('POST', '/api/upload/image');
xhr.send(formData);
});
},
};

return (
<div>
{isUploading && (
<div
style={{
marginBottom: '12px',
padding: '8px',
backgroundColor: '#e3f2fd',
borderRadius: '4px',
}}
>
📤 업로드 중... {uploadProgress}%
<div
style={{
width: '100%',
height: '4px',
backgroundColor: '#e0e0e0',
borderRadius: '2px',
marginTop: '4px',
}}
>
<div
style={{
width: `${uploadProgress}%`,
height: '100%',
backgroundColor: '#2196f3',
borderRadius: '2px',
transition: 'width 0.3s ease',
}}
/>
</div>
</div>
)}

<FroalaEditor
value={content}
onChange={setContent}
customImageUploadOptions={progressUploadOptions}
height={300}
/>
</div>
);
}

이미지 압축 및 리사이징

function CompressedImageUpload() {
const [content, setContent] = useState('');

const compressImage = (file, maxWidth = 1200, quality = 0.8) => {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();

img.onload = () => {
// 비율 유지하며 리사이징
const ratio = Math.min(maxWidth / img.width, maxWidth / img.height);
canvas.width = img.width * ratio;
canvas.height = img.height * ratio;

// 이미지 그리기
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

// 압축된 이미지를 Blob으로 변환
canvas.toBlob(resolve, 'image/jpeg', quality);
};

img.src = URL.createObjectURL(file);
});
};

const compressedUploadOptions = {
imageBeforeUpload: async (editor, fileList, insertImage) => {
try {
// 이미지 압축
const compressedBlob = await compressImage(file);

// 압축된 파일로 FormData 생성
const formData = new FormData();
formData.append('file', compressedBlob, file.name);
formData.append('original_size', file.size);
formData.append('compressed_size', compressedBlob.size);

const response = await fetch('/api/upload/compressed-image', {
method: 'POST',
body: formData,
});

const result = await response.json();

console.log(`압축률: ${Math.round((1 - compressedBlob.size / file.size) * 100)}%`);

insertImage(result.image_url, false, {}, null, {});
} catch (error) {
console.error('압축 업로드 실패:', error);
throw error;
}
},
};

return (
<FroalaEditor
value={content}
onChange={setContent}
customImageUploadOptions={compressedUploadOptions}
height={300}
placeholder="이미지는 자동으로 압축되어 업로드됩니다..."
/>
);
}

워터마크 추가 업로드

function WatermarkUpload() {
const [content, setContent] = useState('');

const addWatermark = (file) => {
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();

img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;

// 원본 이미지 그리기
ctx.drawImage(img, 0, 0);

// 워터마크 추가
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '20px Arial';
ctx.textAlign = 'right';
ctx.fillText('© NCDS Editor', canvas.width - 20, canvas.height - 20);

// 워터마크가 추가된 이미지를 Blob으로 변환
canvas.toBlob(resolve, 'image/jpeg', 0.9);
};

img.src = URL.createObjectURL(file);
});
};

const watermarkUploadOptions = {
imageBeforeUpload: async (editor, fileList, insertImage) => {
try {
// 워터마크 추가
const watermarkedBlob = await addWatermark(file);

// 업로드
const formData = new FormData();
formData.append('file', watermarkedBlob, file.name);

const response = await fetch('/api/upload/watermarked-image', {
method: 'POST',
body: formData,
});

const result = await response.json();
insertImage(result.watermarked_url, false, {}, null, {});
} catch (error) {
console.error('워터마크 업로드 실패:', error);
throw error;
}
},
};

return (
<FroalaEditor
value={content}
onChange={setContent}
customImageUploadOptions={watermarkUploadOptions}
height={300}
placeholder="업로드된 이미지에 자동으로 워터마크가 추가됩니다..."
/>
);
}

오류 처리 및 사용자 피드백

포괄적인 오류 처리

function RobustUpload() {
const [content, setContent] = useState('');
const [notification, setNotification] = useState(null);

const showNotification = (message, type = 'info') => {
setNotification({ message, type });
setTimeout(() => setNotification(null), 3000);
};

const robustUploadOptions = {
imageBeforeUpload: async (editor, fileList, insertImage) => {
try {
// 1. 파일 유효성 검사
if (!file || !file.type.startsWith('image/')) {
throw new Error('이미지 파일만 업로드 가능합니다.');
}

if (file.size > 10 * 1024 * 1024) {
throw new Error('파일 크기는 10MB 이하여야 합니다.');
}

showNotification('이미지 업로드를 시작합니다...', 'info');

// 2. 업로드 시도
const formData = new FormData();
formData.append('file', file);

const response = await fetch('/api/upload/image', {
method: 'POST',
body: formData,
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || '업로드 실패');
}

const result = await response.json();
showNotification('이미지 업로드가 완료되었습니다!', 'success');

insertImage(result.image_url, false, {}, null, {});
} catch (error) {
console.error('업로드 오류:', error);
showNotification(error.message, 'error');
throw error;
}
},
};

return (
<div>
{notification && (
<div
style={{
marginBottom: '12px',
padding: '8px 12px',
borderRadius: '4px',
backgroundColor:
notification.type === 'success' ? '#d4edda' : notification.type === 'error' ? '#f8d7da' : '#e3f2fd',
border: `1px solid ${
notification.type === 'success' ? '#c3e6cb' : notification.type === 'error' ? '#f5c6cb' : '#bbdefb'
}`,
color: notification.type === 'success' ? '#155724' : notification.type === 'error' ? '#721c24' : '#1565c0',
}}
>
{notification.type === 'success' && '✅ '}
{notification.type === 'error' && '❌ '}
{notification.type === 'info' && 'ℹ️ '}
{notification.message}
</div>
)}

<FroalaEditor value={content} onChange={setContent} customImageUploadOptions={robustUploadOptions} height={300} />
</div>
);
}

주요 활용 사례

1. 서버 응답 형식이 표준과 다른 경우

  • 기존 API 시스템과의 호환성 유지
  • 레거시 시스템 연동

2. 파일 처리가 필요한 경우

  • 파일명 자동 변경 (중복 방지)
  • 이미지 전처리 (압축, 리사이징, 워터마크)
  • 메타데이터 추가

3. 업로드 서버 선택이 필요한 경우

  • 파일 크기별 서버 분산
  • 지역별 CDN 선택
  • 로드밸런싱

4. 특별한 검증이 필요한 경우

  • 이미지 내용 분석
  • 바이러스 검사
  • 커스텀 파일 규칙

customImageUploadOptions를 활용하여 프로젝트의 특별한 요구사항에 맞는 이미지 업로드 시스템을 구축해보세요! 🎯