119 lines
3.5 KiB
TypeScript
119 lines
3.5 KiB
TypeScript
import multer from 'multer';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
|
|
// 업로드 디렉토리 경로 (운영: /app/uploads/mail-attachments, 개발: 프로젝트 루트)
|
|
const UPLOAD_DIR = process.env.NODE_ENV === 'production'
|
|
? '/app/uploads/mail-attachments'
|
|
: path.join(process.cwd(), 'uploads', 'mail-attachments');
|
|
|
|
// 디렉토리 생성 (없으면) - try-catch로 권한 에러 방지
|
|
try {
|
|
if (!fs.existsSync(UPLOAD_DIR)) {
|
|
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
|
}
|
|
} catch (error) {
|
|
console.error('메일 첨부파일 디렉토리 생성 실패:', error);
|
|
// 디렉토리가 이미 존재하거나 권한이 없어도 서비스는 계속 실행
|
|
}
|
|
|
|
// 간단한 파일명 정규화 함수 (한글-분석.txt 방식)
|
|
function normalizeFileName(filename: string): string {
|
|
if (!filename) return filename;
|
|
|
|
try {
|
|
// NFC 정규화만 수행 (복잡한 디코딩 제거)
|
|
return filename.normalize('NFC');
|
|
} catch (error) {
|
|
console.error(`Failed to normalize filename: ${filename}`, error);
|
|
return filename;
|
|
}
|
|
}
|
|
|
|
// 파일 저장 설정
|
|
const storage = multer.diskStorage({
|
|
destination: (req, file, cb) => {
|
|
cb(null, UPLOAD_DIR);
|
|
},
|
|
filename: (req, file, cb) => {
|
|
try {
|
|
// 파일명 정규화 (한글-분석.txt 방식)
|
|
file.originalname = file.originalname.normalize('NFC');
|
|
|
|
console.log('File upload - Processing:', {
|
|
original: file.originalname,
|
|
originalHex: Buffer.from(file.originalname).toString('hex'),
|
|
});
|
|
|
|
// UUID + 확장자로 유니크한 파일명 생성
|
|
const uniqueId = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
|
const ext = path.extname(file.originalname);
|
|
const filename = `${uniqueId}${ext}`;
|
|
|
|
console.log('Generated filename:', {
|
|
original: file.originalname,
|
|
generated: filename,
|
|
});
|
|
|
|
cb(null, filename);
|
|
} catch (error) {
|
|
console.error('Filename processing error:', error);
|
|
const fallbackFilename = `${Date.now()}-${Math.round(Math.random() * 1e9)}_error.tmp`;
|
|
cb(null, fallbackFilename);
|
|
}
|
|
},
|
|
});
|
|
|
|
// 파일 필터 (허용할 파일 타입)
|
|
const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
|
|
// 파일명 정규화 (fileFilter가 filename보다 먼저 실행되므로 여기서 먼저 처리)
|
|
try {
|
|
// NFD를 NFC로 정규화만 수행
|
|
file.originalname = file.originalname.normalize('NFC');
|
|
} catch (error) {
|
|
console.warn('Failed to normalize filename in fileFilter:', error);
|
|
}
|
|
|
|
// 위험한 파일 확장자 차단
|
|
const dangerousExtensions = ['.exe', '.bat', '.cmd', '.sh', '.ps1', '.msi'];
|
|
const ext = path.extname(file.originalname).toLowerCase();
|
|
|
|
if (dangerousExtensions.includes(ext)) {
|
|
console.log(`❌ 차단된 파일 타입: ${ext}`);
|
|
cb(new Error(`보안상의 이유로 ${ext} 파일은 첨부할 수 없습니다.`));
|
|
return;
|
|
}
|
|
|
|
cb(null, true);
|
|
};
|
|
|
|
// Multer 설정
|
|
export const uploadMailAttachment = multer({
|
|
storage,
|
|
fileFilter,
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024, // 10MB 제한
|
|
files: 5, // 최대 5개 파일
|
|
},
|
|
});
|
|
|
|
// 첨부파일 정보 추출 헬퍼
|
|
export interface AttachmentInfo {
|
|
filename: string;
|
|
originalName: string;
|
|
size: number;
|
|
path: string;
|
|
mimetype: string;
|
|
}
|
|
|
|
export const extractAttachmentInfo = (files: Express.Multer.File[]): AttachmentInfo[] => {
|
|
return files.map((file) => ({
|
|
filename: file.filename,
|
|
originalName: file.originalname,
|
|
size: file.size,
|
|
path: file.path,
|
|
mimetype: file.mimetype,
|
|
}));
|
|
};
|
|
|