- 사진 2장까지 업로드 지원 - 카메라 촬영 + 갤러리 선택 분리 - 이미지 압축 및 최적화 (ImageUtils) - iPhone .mpo 파일 JPEG 변환 지원 - 카테고리 변경: 치수불량 → 설계미스, 검사미스 추가 - KST 시간대 설정 - URL 해시 처리로 목록관리 페이지 이동 개선 - 로그인 OAuth2 form-data 형식 수정 - 업로드 속도 개선 및 프로그레스바 추가
135 lines
4.6 KiB
JavaScript
135 lines
4.6 KiB
JavaScript
/**
|
|
* 이미지 압축 및 최적화 유틸리티
|
|
*/
|
|
|
|
const ImageUtils = {
|
|
/**
|
|
* 이미지를 압축하고 리사이즈
|
|
* @param {File|Blob|String} source - 이미지 파일, Blob 또는 base64 문자열
|
|
* @param {Object} options - 압축 옵션
|
|
* @returns {Promise<String>} - 압축된 base64 이미지
|
|
*/
|
|
async compressImage(source, options = {}) {
|
|
const {
|
|
maxWidth = 1024, // 최대 너비
|
|
maxHeight = 1024, // 최대 높이
|
|
quality = 0.7, // JPEG 품질 (0-1)
|
|
format = 'jpeg' // 출력 형식
|
|
} = options;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let img = new Image();
|
|
|
|
// 이미지 로드 완료 시
|
|
img.onload = () => {
|
|
// Canvas 생성
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// 리사이즈 계산
|
|
let { width, height } = this.calculateDimensions(
|
|
img.width,
|
|
img.height,
|
|
maxWidth,
|
|
maxHeight
|
|
);
|
|
|
|
// Canvas 크기 설정
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
// 이미지 그리기
|
|
ctx.drawImage(img, 0, 0, width, height);
|
|
|
|
// 압축된 이미지를 base64로 변환
|
|
canvas.toBlob((blob) => {
|
|
if (!blob) {
|
|
reject(new Error('이미지 압축 실패'));
|
|
return;
|
|
}
|
|
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
resolve(reader.result);
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
}, `image/${format}`, quality);
|
|
};
|
|
|
|
img.onerror = () => reject(new Error('이미지 로드 실패'));
|
|
|
|
// 소스 타입에 따라 처리
|
|
if (typeof source === 'string') {
|
|
// Base64 문자열인 경우
|
|
img.src = source;
|
|
} else if (source instanceof File || source instanceof Blob) {
|
|
// File 또는 Blob인 경우
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
img.src = reader.result;
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(source);
|
|
} else {
|
|
reject(new Error('지원하지 않는 이미지 형식'));
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* 이미지 크기 계산 (비율 유지)
|
|
*/
|
|
calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
|
|
// 원본 크기가 제한 내에 있으면 그대로 반환
|
|
if (originalWidth <= maxWidth && originalHeight <= maxHeight) {
|
|
return { width: originalWidth, height: originalHeight };
|
|
}
|
|
|
|
// 비율 계산
|
|
const widthRatio = maxWidth / originalWidth;
|
|
const heightRatio = maxHeight / originalHeight;
|
|
const ratio = Math.min(widthRatio, heightRatio);
|
|
|
|
return {
|
|
width: Math.round(originalWidth * ratio),
|
|
height: Math.round(originalHeight * ratio)
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 파일 크기를 사람이 읽을 수 있는 형식으로 변환
|
|
*/
|
|
formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
},
|
|
|
|
/**
|
|
* Base64 문자열의 크기 계산
|
|
*/
|
|
getBase64Size(base64String) {
|
|
const base64Length = base64String.length - (base64String.indexOf(',') + 1);
|
|
const padding = (base64String.charAt(base64String.length - 2) === '=') ? 2 :
|
|
((base64String.charAt(base64String.length - 1) === '=') ? 1 : 0);
|
|
return (base64Length * 0.75) - padding;
|
|
},
|
|
|
|
/**
|
|
* 이미지 미리보기 생성 (썸네일)
|
|
*/
|
|
async createThumbnail(source, size = 150) {
|
|
return this.compressImage(source, {
|
|
maxWidth: size,
|
|
maxHeight: size,
|
|
quality: 0.8
|
|
});
|
|
}
|
|
};
|
|
|
|
// 전역으로 사용 가능하도록 export
|
|
window.ImageUtils = ImageUtils;
|