100 lines
2.6 KiB
JavaScript
100 lines
2.6 KiB
JavaScript
|
|
import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||
|
|
import sharp from 'sharp';
|
||
|
|
import config from '../config/index.js';
|
||
|
|
|
||
|
|
// S3 클라이언트 생성
|
||
|
|
const s3Client = new S3Client({
|
||
|
|
endpoint: config.s3.endpoint,
|
||
|
|
region: 'us-east-1',
|
||
|
|
credentials: {
|
||
|
|
accessKeyId: config.s3.accessKey,
|
||
|
|
secretAccessKey: config.s3.secretKey,
|
||
|
|
},
|
||
|
|
forcePathStyle: true,
|
||
|
|
});
|
||
|
|
|
||
|
|
const BUCKET = config.s3.bucket;
|
||
|
|
const PUBLIC_URL = config.s3.publicUrl;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 이미지를 3가지 해상도로 변환
|
||
|
|
*/
|
||
|
|
async function processImage(buffer) {
|
||
|
|
const [originalBuffer, mediumBuffer, thumbBuffer] = await Promise.all([
|
||
|
|
sharp(buffer).webp({ lossless: true }).toBuffer(),
|
||
|
|
sharp(buffer)
|
||
|
|
.resize(800, null, { withoutEnlargement: true })
|
||
|
|
.webp({ quality: 85 })
|
||
|
|
.toBuffer(),
|
||
|
|
sharp(buffer)
|
||
|
|
.resize(400, null, { withoutEnlargement: true })
|
||
|
|
.webp({ quality: 80 })
|
||
|
|
.toBuffer(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return { originalBuffer, mediumBuffer, thumbBuffer };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* S3에 이미지 업로드
|
||
|
|
*/
|
||
|
|
async function uploadToS3(key, buffer, contentType = 'image/webp') {
|
||
|
|
await s3Client.send(new PutObjectCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Key: key,
|
||
|
|
Body: buffer,
|
||
|
|
ContentType: contentType,
|
||
|
|
}));
|
||
|
|
return `${PUBLIC_URL}/${BUCKET}/${key}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* S3에서 이미지 삭제
|
||
|
|
*/
|
||
|
|
async function deleteFromS3(key) {
|
||
|
|
try {
|
||
|
|
await s3Client.send(new DeleteObjectCommand({
|
||
|
|
Bucket: BUCKET,
|
||
|
|
Key: key,
|
||
|
|
}));
|
||
|
|
} catch (err) {
|
||
|
|
console.error(`S3 삭제 오류 (${key}):`, err.message);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 멤버 프로필 이미지 업로드
|
||
|
|
* @param {string} name - 멤버 이름
|
||
|
|
* @param {Buffer} buffer - 이미지 버퍼
|
||
|
|
* @returns {Promise<{originalUrl: string, mediumUrl: string, thumbUrl: string}>}
|
||
|
|
*/
|
||
|
|
export async function uploadMemberImage(name, buffer) {
|
||
|
|
const { originalBuffer, mediumBuffer, thumbBuffer } = await processImage(buffer);
|
||
|
|
|
||
|
|
const basePath = `member/${name}`;
|
||
|
|
const filename = `${name}.webp`;
|
||
|
|
|
||
|
|
// 병렬 업로드
|
||
|
|
const [originalUrl, mediumUrl, thumbUrl] = await Promise.all([
|
||
|
|
uploadToS3(`${basePath}/original/${filename}`, originalBuffer),
|
||
|
|
uploadToS3(`${basePath}/medium_800/${filename}`, mediumBuffer),
|
||
|
|
uploadToS3(`${basePath}/thumb_400/${filename}`, thumbBuffer),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return { originalUrl, mediumUrl, thumbUrl };
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 멤버 프로필 이미지 삭제
|
||
|
|
* @param {string} name - 멤버 이름
|
||
|
|
*/
|
||
|
|
export async function deleteMemberImage(name) {
|
||
|
|
const basePath = `member/${name}`;
|
||
|
|
const filename = `${name}.webp`;
|
||
|
|
const sizes = ['original', 'medium_800', 'thumb_400'];
|
||
|
|
|
||
|
|
await Promise.all(
|
||
|
|
sizes.map(size => deleteFromS3(`${basePath}/${size}/${filename}`))
|
||
|
|
);
|
||
|
|
}
|