sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거, department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러, 4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
305 lines
7.6 KiB
JavaScript
305 lines
7.6 KiB
JavaScript
// 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
|
|
};
|