// utils/validator.js - 유효성 검사 유틸리티 const { handleValidationError } = require('./errorHandler'); /** * 필수 필드 검사 */ const required = (value, fieldName) => { if (value === undefined || value === null || value === '') { handleValidationError(fieldName, value, 'required'); } return true; }; /** * 문자열 길이 검사 */ const stringLength = (value, fieldName, min = 0, max = Infinity) => { if (typeof value !== 'string') { handleValidationError(fieldName, value, 'string type'); } if (value.length < min || value.length > max) { handleValidationError(fieldName, value, `length between ${min} and ${max}`); } return true; }; /** * 숫자 범위 검사 */ const numberRange = (value, fieldName, min = -Infinity, max = Infinity) => { const num = parseFloat(value); if (isNaN(num)) { handleValidationError(fieldName, value, 'number type'); } if (num < min || num > max) { handleValidationError(fieldName, value, `number between ${min} and ${max}`); } return true; }; /** * 정수 검사 */ const integer = (value, fieldName) => { const num = parseInt(value); if (isNaN(num) || num.toString() !== value.toString()) { handleValidationError(fieldName, value, 'integer'); } return true; }; /** * 이메일 형식 검사 */ const email = (value, fieldName) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { handleValidationError(fieldName, value, 'valid email format'); } return true; }; /** * 날짜 형식 검사 (YYYY-MM-DD) */ const dateFormat = (value, fieldName) => { const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(value)) { handleValidationError(fieldName, value, 'YYYY-MM-DD format'); } const date = new Date(value); if (isNaN(date.getTime())) { handleValidationError(fieldName, value, 'valid date'); } return true; }; /** * 시간 형식 검사 (HH:MM) */ const timeFormat = (value, fieldName) => { const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/; if (!timeRegex.test(value)) { handleValidationError(fieldName, value, 'HH:MM format'); } return true; }; /** * 열거형 값 검사 */ const enumValue = (value, fieldName, allowedValues) => { if (!allowedValues.includes(value)) { handleValidationError(fieldName, value, `one of: ${allowedValues.join(', ')}`); } return true; }; /** * 배열 검사 */ const arrayType = (value, fieldName, minLength = 0, maxLength = Infinity) => { if (!Array.isArray(value)) { handleValidationError(fieldName, value, 'array type'); } if (value.length < minLength || value.length > maxLength) { handleValidationError(fieldName, value, `array length between ${minLength} and ${maxLength}`); } return true; }; /** * 객체 검사 */ const objectType = (value, fieldName) => { if (typeof value !== 'object' || value === null || Array.isArray(value)) { handleValidationError(fieldName, value, 'object type'); } return true; }; /** * 비밀번호 강도 검사 */ const passwordStrength = (value, fieldName) => { if (typeof value !== 'string') { handleValidationError(fieldName, value, 'string type'); } const minLength = 8; const hasUpperCase = /[A-Z]/.test(value); const hasLowerCase = /[a-z]/.test(value); const hasNumbers = /\d/.test(value); const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value); if (value.length < minLength) { handleValidationError(fieldName, value, `minimum ${minLength} characters`); } const strengthChecks = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecialChar]; const passedChecks = strengthChecks.filter(Boolean).length; if (passedChecks < 3) { handleValidationError(fieldName, value, 'at least 3 of: uppercase, lowercase, numbers, special characters'); } return true; }; /** * 스키마 기반 유효성 검사 */ const validateSchema = (data, schema) => { const errors = []; for (const [field, rules] of Object.entries(schema)) { const value = data[field]; try { // 필수 필드 검사 if (rules.required) { required(value, field); } // 값이 없고 필수가 아니면 다른 검사 스킵 if ((value === undefined || value === null || value === '') && !rules.required) { continue; } // 타입별 검사 if (rules.type === 'string' && rules.minLength !== undefined && rules.maxLength !== undefined) { stringLength(value, field, rules.minLength, rules.maxLength); } if (rules.type === 'number' && rules.min !== undefined && rules.max !== undefined) { numberRange(value, field, rules.min, rules.max); } if (rules.type === 'integer') { integer(value, field); } if (rules.type === 'email') { email(value, field); } if (rules.type === 'date') { dateFormat(value, field); } if (rules.type === 'time') { timeFormat(value, field); } if (rules.type === 'array') { arrayType(value, field, rules.minLength, rules.maxLength); } if (rules.type === 'object') { objectType(value, field); } if (rules.enum) { enumValue(value, field, rules.enum); } if (rules.password) { passwordStrength(value, field); } // 커스텀 검증 함수 if (rules.custom && typeof rules.custom === 'function') { rules.custom(value, field); } } catch (error) { errors.push({ field, value, message: error.message }); } } if (errors.length > 0) { const errorMessage = errors.map(e => `${e.field}: ${e.message}`).join(', '); handleValidationError('validation', 'multiple fields', errorMessage); } return true; }; /** * 일반적인 스키마 정의들 */ const schemas = { // 사용자 생성 createUser: { username: { required: true, type: 'string', minLength: 3, maxLength: 50 }, password: { required: true, password: true }, name: { required: true, type: 'string', minLength: 2, maxLength: 100 }, access_level: { required: true, enum: ['user', 'admin', 'system'] }, user_id: { type: 'integer' } }, // 사용자 업데이트 updateUser: { name: { type: 'string', minLength: 2, maxLength: 100 }, access_level: { enum: ['user', 'admin', 'system'] }, user_id: { type: 'integer' } }, // 비밀번호 변경 changePassword: { currentPassword: { required: true, type: 'string' }, newPassword: { required: true, password: true } }, // 일일 작업 보고서 생성 (배열 형태) createDailyWorkReport: { report_date: { required: true, type: 'date' }, user_id: { required: true, type: 'integer' }, work_entries: { required: true, type: 'array' }, created_by: { type: 'integer' } }, // 프로젝트 생성 createProject: { project_name: { required: true, type: 'string', minLength: 2, maxLength: 200 }, description: { type: 'string', maxLength: 1000 }, start_date: { type: 'date' }, end_date: { type: 'date' } }, // 작업자 생성 createWorker: { worker_name: { required: true, type: 'string', minLength: 2, maxLength: 100 }, position: { type: 'string', maxLength: 100 }, department: { type: 'string', maxLength: 100 }, phone: { type: 'string', maxLength: 20 }, email: { type: 'email' } } }; module.exports = { // 개별 검증 함수들 required, stringLength, numberRange, integer, email, dateFormat, timeFormat, enumValue, arrayType, objectType, passwordStrength, // 스키마 검증 validateSchema, schemas };