/** * 이미지 업로드 서비스 * Base64 인코딩된 이미지를 파일로 저장 * * 사용 전 sharp 패키지 설치 필요: * npm install sharp */ const path = require('path'); const fs = require('fs').promises; const crypto = require('crypto'); // sharp는 선택적으로 사용 (설치되어 있지 않으면 리사이징 없이 저장) let sharp; try { sharp = require('sharp'); } catch (e) { console.warn('sharp 패키지가 설치되어 있지 않습니다. 이미지 리사이징이 비활성화됩니다.'); console.warn('이미지 최적화를 위해 npm install sharp 를 실행하세요.'); } // 업로드 디렉토리 설정 const UPLOAD_DIR = path.join(__dirname, '../public/uploads/issues'); const MAX_SIZE = { width: 1920, height: 1920 }; const QUALITY = 85; /** * 업로드 디렉토리 확인 및 생성 */ async function ensureUploadDir() { try { await fs.access(UPLOAD_DIR); } catch { await fs.mkdir(UPLOAD_DIR, { recursive: true }); } } /** * UUID 생성 (간단한 버전) */ function generateId() { return crypto.randomBytes(4).toString('hex'); } /** * 타임스탬프 문자열 생성 */ function getTimestamp() { const now = new Date(); return now.toISOString().replace(/[-:T]/g, '').slice(0, 14); } /** * Base64 문자열에서 이미지 형식 추출 * @param {string} base64String - Base64 인코딩된 이미지 * @returns {string} 이미지 확장자 (jpg, png, etc) */ function getImageExtension(base64String) { const match = base64String.match(/^data:image\/(\w+);base64,/); if (match) { const format = match[1].toLowerCase(); // jpeg를 jpg로 변환 return format === 'jpeg' ? 'jpg' : format; } return 'jpg'; // 기본값 } /** * Base64 이미지를 파일로 저장 * @param {string} base64String - Base64 인코딩된 이미지 (data:image/...;base64,... 형식) * @param {string} prefix - 파일명 접두사 (예: 'issue', 'resolution') * @returns {Promise} 저장된 파일의 웹 경로 또는 null */ async function saveBase64Image(base64String, prefix = 'issue') { try { if (!base64String || typeof base64String !== 'string') { return null; } // Base64 헤더가 없는 경우 처리 let base64Data = base64String; if (base64String.includes('base64,')) { base64Data = base64String.split('base64,')[1]; } // Base64 디코딩 const buffer = Buffer.from(base64Data, 'base64'); if (buffer.length === 0) { console.error('이미지 데이터가 비어있습니다.'); return null; } // 디렉토리 확인 await ensureUploadDir(); // 파일명 생성 const timestamp = getTimestamp(); const uniqueId = generateId(); const extension = 'jpg'; // 모든 이미지를 JPEG로 저장 const filename = `${prefix}_${timestamp}_${uniqueId}.${extension}`; const filepath = path.join(UPLOAD_DIR, filename); // sharp가 설치되어 있으면 리사이징 및 최적화 if (sharp) { try { await sharp(buffer) .resize(MAX_SIZE.width, MAX_SIZE.height, { fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: QUALITY }) .toFile(filepath); } catch (sharpError) { console.error('sharp 처리 실패, 원본 저장:', sharpError.message); // sharp 실패 시 원본 저장 await fs.writeFile(filepath, buffer); } } else { // sharp가 없으면 원본 그대로 저장 await fs.writeFile(filepath, buffer); } // 웹 접근 경로 반환 return `/uploads/issues/${filename}`; } catch (error) { console.error('이미지 저장 실패:', error); return null; } } /** * 여러 Base64 이미지를 한번에 저장 * @param {string[]} base64Images - Base64 이미지 배열 * @param {string} prefix - 파일명 접두사 * @returns {Promise} 저장된 파일 경로 배열 */ async function saveMultipleImages(base64Images, prefix = 'issue') { const paths = []; for (const base64 of base64Images) { if (base64) { const savedPath = await saveBase64Image(base64, prefix); if (savedPath) { paths.push(savedPath); } } } return paths; } /** * 파일 삭제 * @param {string} webPath - 웹 경로 (예: /uploads/issues/filename.jpg) * @returns {Promise} 삭제 성공 여부 */ async function deleteFile(webPath) { try { if (!webPath || typeof webPath !== 'string') { return false; } // 보안: uploads 경로만 삭제 허용 if (!webPath.startsWith('/uploads/')) { console.error('삭제 불가: uploads 외부 경로', webPath); return false; } const filename = path.basename(webPath); const fullPath = path.join(UPLOAD_DIR, filename); try { await fs.access(fullPath); await fs.unlink(fullPath); return true; } catch (accessError) { // 파일이 없으면 성공으로 처리 if (accessError.code === 'ENOENT') { return true; } throw accessError; } } catch (error) { console.error('파일 삭제 실패:', error); return false; } } /** * 여러 파일 삭제 * @param {string[]} webPaths - 웹 경로 배열 * @returns {Promise} */ async function deleteMultipleFiles(webPaths) { for (const webPath of webPaths) { if (webPath) { await deleteFile(webPath); } } } module.exports = { saveBase64Image, saveMultipleImages, deleteFile, deleteMultipleFiles, UPLOAD_DIR };