Files
ev-charger-as/frontend/static/js/imageCompress.js
2026-04-18 06:18:58 +09:00

175 lines
6.0 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* imageCompress.js
* 업로드 전 브라우저에서 이미지를 리사이즈 + JPEG 압축
* Canvas API 사용 — 외부 라이브러리 불필요
*/
const ImageCompressor = (() => {
// 서버에서 가져온 설정 캐시
let _cfg = null;
/** 관리자가 저장한 이미지 설정 로드 (최초 1회만 API 호출) */
async function loadConfig() {
if (_cfg) return _cfg;
try {
const res = await fetch('/api/settings/public');
_cfg = await res.json();
} catch {
_cfg = { image_compress_enabled: true, image_max_px: 1024, image_quality: 85 };
}
return _cfg;
}
/**
* File 객체 하나를 압축해서 새 File 로 반환
* @param {File} file - 원본 이미지 파일
* @param {Object} cfg - { image_compress_enabled, image_max_px, image_quality }
* @returns {Promise<File>}
*/
function compressOne(file, cfg) {
return new Promise((resolve) => {
// 압축 비활성 or 이미지가 아닌 파일은 그대로 반환
if (!cfg.image_compress_enabled || !file.type.startsWith('image/')) {
return resolve(file);
}
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const maxPx = cfg.image_max_px;
let { width, height } = img;
// 긴 변이 maxPx 초과하면 비율 유지하며 축소
if (width > maxPx || height > maxPx) {
if (width >= height) {
height = Math.round((height / width) * maxPx);
width = maxPx;
} else {
width = Math.round((width / height) * maxPx);
height = maxPx;
}
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
const compressed = new File(
[blob],
file.name.replace(/\.[^.]+$/, '') + '.jpg',
{ type: 'image/jpeg', lastModified: Date.now() }
);
resolve(compressed);
},
'image/jpeg',
cfg.image_quality / 100 // 0~1 범위
);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
/**
* FileList / File[] 전체를 압축
* @param {FileList|File[]} files
* @returns {Promise<File[]>}
*/
async function compressAll(files) {
const cfg = await loadConfig();
return Promise.all(Array.from(files).map(f => compressOne(f, cfg)));
}
/**
* input[type=file] + 미리보기 div 에 압축 프리뷰 설정
* @param {string} inputId - file input 요소 id
* @param {string} previewId - 미리보기 컨테이너 id
* @param {string} infoId - 용량 정보 표시 span id (선택)
*/
function setupPreview(inputId, previewId, infoId) {
const input = document.getElementById(inputId);
const preview = document.getElementById(previewId);
const info = infoId ? document.getElementById(infoId) : null;
if (!input || !preview) return;
input.addEventListener('change', async function () {
preview.innerHTML = '';
if (info) info.textContent = '압축 중...';
const cfg = await loadConfig();
const origBytes = Array.from(this.files).reduce((s, f) => s + f.size, 0);
const compressed = await compressAll(this.files);
const compBytes = compressed.reduce((s, f) => s + f.size, 0);
// DataTransfer 로 input.files 교체 (압축된 파일로)
const dt = new DataTransfer();
compressed.forEach(f => dt.items.add(f));
this.files = dt.files;
// 미리보기 렌더링
compressed.forEach((f, i) => {
const url = URL.createObjectURL(f);
const wrap = document.createElement('div');
wrap.style.cssText = 'position:relative;display:inline-block;';
const img = document.createElement('img');
img.src = url;
img.style.cssText = 'width:80px;height:80px;object-fit:cover;border-radius:6px;border:1px solid #C5CFE0;';
img.onload = () => URL.revokeObjectURL(url);
// 삭제 버튼
const del = document.createElement('button');
del.textContent = '×';
del.style.cssText = 'position:absolute;top:-4px;right:-4px;width:18px;height:18px;border-radius:50%;background:#E53935;color:white;border:none;font-size:11px;cursor:pointer;display:flex;align-items:center;justify-content:center;line-height:1;padding:0;';
del.onclick = () => {
// 해당 파일 제거
const cur = Array.from(input.files);
cur.splice(i, 1);
const dt2 = new DataTransfer();
cur.forEach(f2 => dt2.items.add(f2));
input.files = dt2.files;
wrap.remove();
updateInfo(input, info);
};
wrap.appendChild(img);
wrap.appendChild(del);
preview.appendChild(wrap);
});
// 용량 정보
if (info) {
const pct = origBytes > 0 ? Math.round((1 - compBytes / origBytes) * 100) : 0;
if (cfg.image_compress_enabled && pct > 0) {
info.textContent = `${compressed.length}장 | ${fmt(origBytes)}${fmt(compBytes)} (${pct}% 절약) | 최대 ${cfg.image_max_px}px / 품질 ${cfg.image_quality}%`;
info.style.color = '#00875A';
} else {
info.textContent = `${compressed.length}장 | ${fmt(compBytes)} (압축 비활성)`;
info.style.color = '#8899BB';
}
}
});
}
function updateInfo(input, info) {
if (!info) return;
const bytes = Array.from(input.files).reduce((s, f) => s + f.size, 0);
info.textContent = `${input.files.length}장 | ${fmt(bytes)}`;
}
function fmt(bytes) {
return bytes < 1024 * 1024
? (bytes / 1024).toFixed(0) + ' KB'
: (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
return { compressAll, setupPreview, loadConfig };
})();