Files
TK-FB-Project/api.hyungi.net/utils/validator.js
Hyungi Ahn de427c457b feat: 작업 분석 시스템 및 관리 기능 대폭 개선
 새로운 기능:
- 작업 분석 페이지 구현 (기간별, 프로젝트별, 작업자별, 오류별)
- 개별 분석 실행 버튼으로 API 부하 최적화
- 연차/휴무 집계 방식 개선 (주말 제외, 작업내용 통합)
- 프로젝트 관리 시스템 (활성화/비활성화)
- 작업자 관리 시스템 (CRUD 기능)
- 코드 관리 시스템 (작업유형, 작업상태, 오류유형)

🎨 UI/UX 개선:
- 기간별 작업 현황을 테이블 형태로 변경
- 작업자별 rowspan 그룹화로 가독성 향상
- 연차/휴무 프로젝트 하단 배치 및 시각적 구분
- 기간 확정 시스템으로 사용자 경험 개선
- 반응형 디자인 적용

🔧 기술적 개선:
- Rate Limiting 제거 (내부 시스템 최적화)
- 주말 연차/휴무 자동 제외 로직
- 작업공수 계산 정확도 향상
- 데이터베이스 마이그레이션 추가
- API 엔드포인트 확장 및 최적화

🐛 버그 수정:
- projectSelect 요소 참조 오류 해결
- 차트 높이 무한 증가 문제 해결
- 날짜 표시 형식 단순화
- 작업보고서 저장 validation 오류 수정
2025-11-04 16:56:47 +09:00

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'] },
worker_id: { type: 'integer' }
},
// 사용자 업데이트
updateUser: {
name: { type: 'string', minLength: 2, maxLength: 100 },
access_level: { enum: ['user', 'admin', 'system'] },
worker_id: { type: 'integer' }
},
// 비밀번호 변경
changePassword: {
currentPassword: { required: true, type: 'string' },
newPassword: { required: true, password: true }
},
// 일일 작업 보고서 생성 (배열 형태)
createDailyWorkReport: {
report_date: { required: true, type: 'date' },
worker_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
};