- gateway: 로그인 페이지 자동 리다이렉트 시 SSO 쿠키 재설정 + Cache-Control no-store - tkreport(system2): SW 해제, 401 핸들러 리다이렉트 제거, 루프 방지, localStorage 백업 - TKQC 모바일(system3): mCheckAuth를 authManager 위임으로 변경, 루프 방지 - TKQC 공통(system3): api.js 로그인 URL 캐시 버스팅, auth-manager localStorage 백업 - tkuser: SW 해제, 401 핸들러 수정, 루프 방지, localStorage 백업, requireAdmin role 소문자 통일 - system1: 작업보고서 admin role 대소문자 무시, refresh 토큰에 role 필드 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
/**
|
|
* 일일 작업 보고서 서비스
|
|
*
|
|
* 작업 보고서 관련 비즈니스 로직 처리
|
|
*
|
|
* @author TK-FB-Project
|
|
* @since 2025-12-11
|
|
*/
|
|
|
|
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
|
const { ValidationError, NotFoundError, ForbiddenError, DatabaseError } = require('../utils/errors');
|
|
const logger = require('../utils/logger');
|
|
|
|
/**
|
|
* 일일 작업 보고서를 생성하는 비즈니스 로직을 처리합니다.
|
|
* @param {object} reportData - 컨트롤러에서 전달된 보고서 데이터
|
|
* @returns {Promise<object>} 생성 결과 또는 에러
|
|
*/
|
|
const createDailyWorkReportService = async (reportData) => {
|
|
const { report_date, user_id, work_entries, created_by, created_by_name } = reportData;
|
|
|
|
// 1. 기본 유효성 검사
|
|
if (!report_date || !user_id || !work_entries) {
|
|
throw new ValidationError('필수 필드가 누락되었습니다', {
|
|
required: ['report_date', 'user_id', 'work_entries'],
|
|
received: { report_date, user_id, work_entries: !!work_entries }
|
|
});
|
|
}
|
|
|
|
if (!Array.isArray(work_entries) || work_entries.length === 0) {
|
|
throw new ValidationError('최소 하나의 작업 항목이 필요합니다');
|
|
}
|
|
|
|
if (!created_by) {
|
|
throw new ValidationError('사용자 인증 정보가 없습니다');
|
|
}
|
|
|
|
// 2. 작업 항목 유효성 검사
|
|
for (let i = 0; i < work_entries.length; i++) {
|
|
const entry = work_entries[i];
|
|
const requiredFields = ['project_id', 'task_id', 'work_hours'];
|
|
|
|
for (const field of requiredFields) {
|
|
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
|
throw new ValidationError(`작업 항목 ${i + 1}의 필수 필드가 누락되었습니다`, {
|
|
entry: i + 1,
|
|
missingField: field
|
|
});
|
|
}
|
|
}
|
|
|
|
// is_error가 true일 때 error_type_code_id 필수 검사
|
|
if (entry.is_error === true && !entry.error_type_code_id) {
|
|
throw new ValidationError(`에러 상태인 경우 에러 타입이 필요합니다`, {
|
|
entry: i + 1,
|
|
is_error: true
|
|
});
|
|
}
|
|
|
|
const hours = parseFloat(entry.work_hours);
|
|
if (isNaN(hours) || hours <= 0 || hours > 24) {
|
|
throw new ValidationError(`작업 시간이 유효하지 않습니다 (0 초과 24 이하)`, {
|
|
entry: i + 1,
|
|
work_hours: entry.work_hours,
|
|
valid_range: '0 < hours <= 24'
|
|
});
|
|
}
|
|
}
|
|
|
|
// 3. 모델에 전달할 데이터 준비
|
|
const modelData = {
|
|
report_date,
|
|
user_id: parseInt(user_id),
|
|
entries: work_entries.map(entry => ({
|
|
project_id: entry.project_id,
|
|
work_type_id: entry.task_id, // task_id를 work_type_id로 매핑
|
|
work_hours: parseFloat(entry.work_hours),
|
|
work_status_id: entry.work_status_id,
|
|
error_type_id: entry.error_type_id,
|
|
created_by: created_by
|
|
}))
|
|
};
|
|
|
|
logger.info('작업보고서 생성 요청', {
|
|
date: report_date,
|
|
worker: user_id,
|
|
creator: created_by_name,
|
|
entries_count: modelData.entries.length
|
|
});
|
|
|
|
// 4. 모델 함수 호출
|
|
try {
|
|
const result = await dailyWorkReportModel.createReportEntries(modelData);
|
|
|
|
logger.info('작업보고서 생성 성공', {
|
|
report_id: result.report_id,
|
|
entries_count: modelData.entries.length
|
|
});
|
|
|
|
return {
|
|
message: '작업보고서가 성공적으로 생성되었습니다.',
|
|
...result
|
|
};
|
|
} catch (error) {
|
|
logger.error('작업보고서 생성 실패', {
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
throw new DatabaseError('작업보고서 생성 중 데이터베이스 오류가 발생했습니다');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 사용자 권한과 요청 파라미터에 따라 일일 작업 보고서를 조회하는 비즈니스 로직을 처리합니다.
|
|
* @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터
|
|
* @param {object} userInfo - 요청을 보낸 사용자의 정보 (id, role 등)
|
|
* @returns {Promise<Array>} 조회된 작업 보고서 배열
|
|
*/
|
|
const getDailyWorkReportsService = async (queryParams, userInfo) => {
|
|
const { date, start_date, end_date, user_id, created_by: requested_created_by, view_all } = queryParams;
|
|
const { user_id: current_user_id, role } = userInfo;
|
|
|
|
// 날짜 또는 날짜 범위 중 하나는 필수
|
|
if (!date && (!start_date || !end_date)) {
|
|
throw new ValidationError('날짜 또는 날짜 범위가 필요합니다', {
|
|
required: 'date OR (start_date AND end_date)',
|
|
received: { date, start_date, end_date }
|
|
});
|
|
}
|
|
|
|
// 관리자 여부 확인 (대소문자 무시)
|
|
const roleLower = (role || '').toLowerCase();
|
|
const isAdmin = roleLower === 'system' || roleLower === 'admin' || roleLower === 'system admin';
|
|
const canViewAll = isAdmin || view_all === 'true';
|
|
|
|
// 모델에 전달할 조회 옵션 객체 생성
|
|
const options = {};
|
|
|
|
if (date) {
|
|
options.date = date;
|
|
} else {
|
|
options.start_date = start_date;
|
|
options.end_date = end_date;
|
|
}
|
|
|
|
if (user_id) {
|
|
options.user_id = parseInt(user_id);
|
|
}
|
|
|
|
// 최종적으로 필터링할 작성자 ID 결정
|
|
if (!canViewAll) {
|
|
// 관리자가 아니면 자신의 데이터만 보거나, 명시적으로 요청된 자신의 ID만 허용
|
|
options.created_by_user_id = requested_created_by ? Math.min(requested_created_by, current_user_id) : current_user_id;
|
|
} else if (requested_created_by) {
|
|
// 관리자는 다른 사람의 데이터도 조회 가능
|
|
options.created_by_user_id = parseInt(requested_created_by);
|
|
}
|
|
// created_by_user_id가 명시되지 않으면 모든 작성자의 데이터를 조회
|
|
|
|
logger.info('작업보고서 조회 요청', { ...options, requester: current_user_id, isAdmin });
|
|
|
|
try {
|
|
const reports = await dailyWorkReportModel.getReportsWithOptions(options);
|
|
logger.info('작업보고서 조회 성공', { count: reports.length });
|
|
return reports;
|
|
} catch (error) {
|
|
logger.error('작업보고서 조회 실패', { error: error.message });
|
|
throw new DatabaseError('작업보고서 조회 중 데이터베이스 오류가 발생했습니다');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 특정 작업 보고서 항목을 수정하는 비즈니스 로직을 처리합니다.
|
|
* @param {string} reportId - 수정할 보고서의 ID
|
|
* @param {object} updateData - 수정할 데이터
|
|
* @param {object} userInfo - 요청을 보낸 사용자의 정보
|
|
* @returns {Promise<object>} 수정 결과
|
|
*/
|
|
const updateWorkReportService = async (reportId, updateData, userInfo) => {
|
|
const { user_id: updated_by } = userInfo;
|
|
|
|
if (!reportId || !updateData || Object.keys(updateData).length === 0) {
|
|
throw new ValidationError('보고서 ID와 수정할 데이터가 필요합니다', {
|
|
reportId,
|
|
hasUpdateData: !!updateData,
|
|
updateFieldsCount: updateData ? Object.keys(updateData).length : 0
|
|
});
|
|
}
|
|
|
|
const modelUpdateData = { ...updateData, updated_by_user_id: updated_by };
|
|
|
|
logger.info('작업보고서 수정 요청', { reportId, updatedBy: updated_by });
|
|
|
|
try {
|
|
const affectedRows = await dailyWorkReportModel.updateReportById(reportId, modelUpdateData);
|
|
|
|
if (affectedRows === 0) {
|
|
throw new NotFoundError('수정할 작업보고서를 찾을 수 없습니다');
|
|
}
|
|
|
|
logger.info('작업보고서 수정 성공', { reportId });
|
|
return {
|
|
message: '작업보고서가 성공적으로 수정되었습니다.',
|
|
report_id: reportId,
|
|
affected_rows: affectedRows
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof NotFoundError) {
|
|
throw error;
|
|
}
|
|
logger.error('작업보고서 수정 실패', { reportId, error: error.message });
|
|
throw new DatabaseError('작업보고서 수정 중 데이터베이스 오류가 발생했습니다');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 특정 작업 보고서 항목을 삭제하는 비즈니스 로직을 처리합니다.
|
|
* @param {string} reportId - 삭제할 보고서의 ID
|
|
* @param {object} userInfo - 요청을 보낸 사용자의 정보
|
|
* @returns {Promise<object>} 삭제 결과
|
|
*/
|
|
const removeDailyWorkReportService = async (reportId, userInfo) => {
|
|
const { user_id: deleted_by } = userInfo;
|
|
|
|
if (!reportId) {
|
|
throw new ValidationError('삭제할 보고서 ID가 필요합니다');
|
|
}
|
|
|
|
logger.info('작업보고서 삭제 요청', { reportId, deletedBy: deleted_by });
|
|
|
|
try {
|
|
const affectedRows = await dailyWorkReportModel.removeReportById(reportId, deleted_by);
|
|
|
|
if (affectedRows === 0) {
|
|
throw new NotFoundError('삭제할 작업보고서를 찾을 수 없습니다');
|
|
}
|
|
|
|
logger.info('작업보고서 삭제 성공', { reportId });
|
|
return {
|
|
message: '작업보고서가 성공적으로 삭제되었습니다.',
|
|
report_id: reportId,
|
|
affected_rows: affectedRows
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof NotFoundError) {
|
|
throw error;
|
|
}
|
|
logger.error('작업보고서 삭제 실패', { reportId, error: error.message });
|
|
throw new DatabaseError('작업보고서 삭제 중 데이터베이스 오류가 발생했습니다');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 기간별 작업 보고서 통계를 조회하는 비즈니스 로직을 처리합니다.
|
|
* @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 (start_date, end_date)
|
|
* @returns {Promise<object>} 통계 데이터
|
|
*/
|
|
const getStatisticsService = async (queryParams) => {
|
|
const { start_date, end_date } = queryParams;
|
|
|
|
if (!start_date || !end_date) {
|
|
throw new ValidationError('시작일과 종료일이 모두 필요합니다', {
|
|
required: ['start_date', 'end_date'],
|
|
received: { start_date, end_date }
|
|
});
|
|
}
|
|
|
|
logger.info('작업보고서 통계 조회 요청', { start_date, end_date });
|
|
|
|
try {
|
|
const statsData = await dailyWorkReportModel.getStatistics(start_date, end_date);
|
|
|
|
logger.info('작업보고서 통계 조회 성공', { period: `${start_date} ~ ${end_date}` });
|
|
return {
|
|
...statsData,
|
|
metadata: {
|
|
period: `${start_date} ~ ${end_date}`,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
};
|
|
} catch (error) {
|
|
logger.error('통계 조회 실패', { error: error.message });
|
|
throw new DatabaseError('통계 조회 중 데이터베이스 오류가 발생했습니다');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 일일 또는 작업자별 작업 요약 정보를 조회하는 비즈니스 로직을 처리합니다.
|
|
* @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 (date 또는 user_id)
|
|
* @returns {Promise<object>} 요약 데이터
|
|
*/
|
|
const getSummaryService = async (queryParams) => {
|
|
const { date, user_id } = queryParams;
|
|
|
|
if (!date && !user_id) {
|
|
throw new ValidationError('날짜 또는 작업자 ID가 필요합니다', {
|
|
required: 'date OR user_id',
|
|
received: { date, user_id }
|
|
});
|
|
}
|
|
|
|
try {
|
|
if (date) {
|
|
logger.info('일일 요약 조회 요청', { date });
|
|
const result = await dailyWorkReportModel.getSummaryByDate(date);
|
|
logger.info('일일 요약 조회 성공', { date });
|
|
return result;
|
|
} else { // user_id
|
|
logger.info('작업자별 요약 조회 요청', { user_id });
|
|
const result = await dailyWorkReportModel.getSummaryByWorker(user_id);
|
|
logger.info('작업자별 요약 조회 성공', { user_id });
|
|
return result;
|
|
}
|
|
} catch (error) {
|
|
logger.error('요약 정보 조회 실패', { error: error.message });
|
|
throw new DatabaseError('요약 정보 조회 중 데이터베이스 오류가 발생했습니다');
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
createDailyWorkReportService,
|
|
getDailyWorkReportsService,
|
|
updateWorkReportService,
|
|
removeDailyWorkReportService,
|
|
getStatisticsService,
|
|
getSummaryService,
|
|
};
|