Files
sst_site/middleware/validation.js
2025-10-19 18:27:00 +09:00

310 lines
8.6 KiB
JavaScript

/**
* Validation middleware for various data types
*/
const { body, validationResult } = require('express-validator');
/**
* Validation error handler
*/
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
// For API requests
if (req.xhr || req.headers.accept?.includes('application/json')) {
return res.status(400).json({
success: false,
message: 'Validation failed',
errors: errors.array()
});
}
// For web requests
const errorMessages = errors.array().map(error => error.msg);
req.flash('error', errorMessages);
return res.redirect('back');
}
next();
};
/**
* Contact form validation
*/
const validateContactForm = [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('이름은 2-50자 사이여야 합니다.')
.matches(/^[a-zA-Z가-힣\s]+$/)
.withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'),
body('email')
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.')
.normalizeEmail(),
body('phone')
.optional()
.matches(/^[0-9\-\+\(\)\s]+$/)
.withMessage('유효한 전화번호를 입력해주세요.'),
body('company')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('회사명은 100자 이하여야 합니다.'),
body('service')
.isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'digital-marketing', 'consulting', 'other'])
.withMessage('유효한 서비스를 선택해주세요.'),
body('budget')
.optional()
.isIn(['under-500', '500-1000', '1000-3000', '3000-5000', '5000-10000', 'over-10000', 'discuss'])
.withMessage('유효한 예산 범위를 선택해주세요.'),
body('message')
.trim()
.isLength({ min: 10, max: 2000 })
.withMessage('메시지는 10-2000자 사이여야 합니다.'),
handleValidationErrors
];
/**
* User registration validation
*/
const validateRegistration = [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('이름은 2-50자 사이여야 합니다.')
.matches(/^[a-zA-Z가-힣\s]+$/)
.withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'),
body('email')
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.')
.normalizeEmail(),
body('password')
.isLength({ min: 8 })
.withMessage('비밀번호는 최소 8자 이상이어야 합니다.')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
.withMessage('비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다.'),
body('confirmPassword')
.custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('비밀번호 확인이 일치하지 않습니다.');
}
return true;
}),
handleValidationErrors
];
/**
* User login validation
*/
const validateLogin = [
body('email')
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.')
.normalizeEmail(),
body('password')
.isLength({ min: 1 })
.withMessage('비밀번호를 입력해주세요.'),
handleValidationErrors
];
/**
* Portfolio validation
*/
const validatePortfolio = [
body('title')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('제목은 2-100자 사이여야 합니다.'),
body('description')
.trim()
.isLength({ min: 10, max: 5000 })
.withMessage('설명은 10-5000자 사이여야 합니다.'),
body('shortDescription')
.optional()
.trim()
.isLength({ max: 200 })
.withMessage('간단한 설명은 200자 이하여야 합니다.'),
body('category')
.isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'marketing'])
.withMessage('유효한 카테고리를 선택해주세요.'),
body('technologies')
.optional()
.isArray()
.withMessage('기술 스택은 배열이어야 합니다.'),
body('clientName')
.optional()
.trim()
.isLength({ max: 100 })
.withMessage('클라이언트 이름은 100자 이하여야 합니다.'),
body('projectUrl')
.optional()
.isURL()
.withMessage('유효한 URL을 입력해주세요.'),
body('status')
.optional()
.isIn(['planning', 'in-progress', 'completed', 'on-hold'])
.withMessage('유효한 상태를 선택해주세요.'),
handleValidationErrors
];
/**
* Service validation
*/
const validateService = [
body('name')
.trim()
.isLength({ min: 2, max: 100 })
.withMessage('서비스명은 2-100자 사이여야 합니다.'),
body('description')
.trim()
.isLength({ min: 10, max: 5000 })
.withMessage('설명은 10-5000자 사이여야 합니다.'),
body('shortDescription')
.optional()
.trim()
.isLength({ max: 200 })
.withMessage('간단한 설명은 200자 이하여야 합니다.'),
body('category')
.isIn(['development', 'design', 'marketing', 'consulting'])
.withMessage('유효한 카테고리를 선택해주세요.'),
body('pricing.basePrice')
.optional()
.isNumeric()
.withMessage('기본 가격은 숫자여야 합니다.'),
body('pricing.priceType')
.optional()
.isIn(['project', 'hourly', 'monthly'])
.withMessage('유효한 가격 유형을 선택해주세요.'),
handleValidationErrors
];
/**
* Calculator validation
*/
const validateCalculator = [
body('service')
.isMongoId()
.withMessage('유효한 서비스를 선택해주세요.'),
body('projectType')
.optional()
.isIn(['simple', 'medium', 'complex', 'enterprise'])
.withMessage('유효한 프로젝트 유형을 선택해주세요.'),
body('timeline')
.optional()
.isIn(['urgent', 'normal', 'flexible'])
.withMessage('유효한 타임라인을 선택해주세요.'),
body('features')
.optional()
.isArray()
.withMessage('기능은 배열이어야 합니다.'),
body('contactInfo.name')
.optional()
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('이름은 2-50자 사이여야 합니다.'),
body('contactInfo.email')
.optional()
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.'),
body('contactInfo.phone')
.optional()
.matches(/^[0-9\-\+\(\)\s]+$/)
.withMessage('유효한 전화번호를 입력해주세요.'),
handleValidationErrors
];
/**
* Settings validation
*/
const validateSettings = [
body('siteName')
.optional()
.trim()
.isLength({ min: 1, max: 100 })
.withMessage('사이트명은 1-100자 사이여야 합니다.'),
body('siteDescription')
.optional()
.trim()
.isLength({ max: 500 })
.withMessage('사이트 설명은 500자 이하여야 합니다.'),
body('contact.email')
.optional()
.isEmail()
.withMessage('유효한 이메일 주소를 입력해주세요.'),
body('contact.phone')
.optional()
.matches(/^[0-9\-\+\(\)\s]+$/)
.withMessage('유효한 전화번호를 입력해주세요.'),
body('social.facebook')
.optional()
.isURL()
.withMessage('유효한 Facebook URL을 입력해주세요.'),
body('social.twitter')
.optional()
.isURL()
.withMessage('유효한 Twitter URL을 입력해주세요.'),
body('social.linkedin')
.optional()
.isURL()
.withMessage('유효한 LinkedIn URL을 입력해주세요.'),
body('social.instagram')
.optional()
.isURL()
.withMessage('유효한 Instagram URL을 입력해주세요.'),
handleValidationErrors
];
module.exports = {
handleValidationErrors,
validateContactForm,
validateRegistration,
validateLogin,
validatePortfolio,
validateService,
validateCalculator,
validateSettings
};