feat(backend): 프로젝트 분석 API 구현
- 프론트엔드의 데이터 가공 로직을 백엔드로 이전하여 성능 개선 - 단일 API 호출로 모든 집계 데이터를 반환하도록 C-S-M 아키텍처 구현
This commit is contained in:
22
api.hyungi.net/controllers/analysisController.js
Normal file
22
api.hyungi.net/controllers/analysisController.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// /controllers/analysisController.js
|
||||||
|
const analysisService = require('../services/analysisService');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프로젝트 분석 데이터를 조회하는 API 요청을 처리합니다.
|
||||||
|
*/
|
||||||
|
const getAnalysisData = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
|
||||||
|
const data = await analysisService.getAnalysisService(startDate, endDate);
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('💥 분석 데이터 컨트롤러 오류:', err);
|
||||||
|
res.status(400).json({ success: false, error: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAnalysisData,
|
||||||
|
};
|
||||||
@@ -135,6 +135,7 @@ const healthRoutes = require('./routes/healthRoutes');
|
|||||||
const pipeSpecRoutes = require('./routes/pipeSpecRoutes');
|
const pipeSpecRoutes = require('./routes/pipeSpecRoutes');
|
||||||
const dailyWorkReportRoutes = require('./routes/dailyWorkReportRoutes');
|
const dailyWorkReportRoutes = require('./routes/dailyWorkReportRoutes');
|
||||||
const workAnalysisRoutes = require('./routes/workAnalysisRoutes');
|
const workAnalysisRoutes = require('./routes/workAnalysisRoutes');
|
||||||
|
const analysisRoutes = require('./routes/analysisRoutes'); // 새로운 분석 라우트
|
||||||
|
|
||||||
// 🔒 인증 미들웨어 가져오기
|
// 🔒 인증 미들웨어 가져오기
|
||||||
const { verifyToken } = require('./middlewares/authMiddleware');
|
const { verifyToken } = require('./middlewares/authMiddleware');
|
||||||
@@ -243,6 +244,7 @@ app.use('/api/issue-types', issueTypeRoutes);
|
|||||||
app.use('/api/workers', workerRoutes);
|
app.use('/api/workers', workerRoutes);
|
||||||
app.use('/api/daily-work-reports', dailyWorkReportRoutes);
|
app.use('/api/daily-work-reports', dailyWorkReportRoutes);
|
||||||
app.use('/api/work-analysis', workAnalysisRoutes);
|
app.use('/api/work-analysis', workAnalysisRoutes);
|
||||||
|
app.use('/api/analysis', analysisRoutes); // 새로운 분석 라우트 등록
|
||||||
|
|
||||||
// 📊 리포트 및 분석
|
// 📊 리포트 및 분석
|
||||||
app.use('/api/workreports', workReportRoutes);
|
app.use('/api/workreports', workReportRoutes);
|
||||||
|
|||||||
115
api.hyungi.net/models/analysisModel.js
Normal file
115
api.hyungi.net/models/analysisModel.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// /models/analysisModel.js
|
||||||
|
const { getDb } = require('../dbPool');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지정된 기간 동안의 작업 보고서 데이터를 집계하여 분석합니다.
|
||||||
|
* 이 함수는 여러 개의 SQL 쿼리를 병렬로 실행하여 효율성을 높입니다.
|
||||||
|
* @param {string} startDate - 시작일 (YYYY-MM-DD)
|
||||||
|
* @param {string} endDate - 종료일 (YYYY-MM-DD)
|
||||||
|
* @returns {Promise<object>} - 요약, 프로젝트별, 작업자별, 작업별 집계 데이터 및 상세 내역
|
||||||
|
*/
|
||||||
|
const getAnalysis = async (startDate, endDate) => {
|
||||||
|
try {
|
||||||
|
const db = await getDb();
|
||||||
|
|
||||||
|
// SQL 쿼리에서 반복적으로 사용될 WHERE 조건과 실제 투입 시간 계산 로직
|
||||||
|
const whereClause = `WHERE dwr.report_date BETWEEN ? AND ?`;
|
||||||
|
const workHoursCalc = `
|
||||||
|
CASE dwr.work_details
|
||||||
|
WHEN '연차' THEN 0 WHEN '반차' THEN 4 WHEN '반반차' THEN 6
|
||||||
|
WHEN '조퇴' THEN 2 WHEN '휴무' THEN 0 WHEN '유급' THEN 0
|
||||||
|
ELSE 8
|
||||||
|
END + (COALESCE(dwr.overtime_hours, 0) * 1.5)
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 1. 요약 정보 쿼리
|
||||||
|
const summarySql = `
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT dwr.project_id) as totalProjects,
|
||||||
|
COUNT(DISTINCT dwr.worker_id) as totalWorkers,
|
||||||
|
COUNT(DISTINCT dwr.task_id) as totalTasks,
|
||||||
|
SUM(${workHoursCalc}) as totalHours
|
||||||
|
FROM DailyWorkReports dwr
|
||||||
|
${whereClause} AND (${workHoursCalc}) > 0
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 2. 프로젝트별 집계 쿼리
|
||||||
|
const byProjectSql = `
|
||||||
|
SELECT p.project_name as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.worker_id) as participants
|
||||||
|
FROM DailyWorkReports dwr
|
||||||
|
JOIN Projects p ON dwr.project_id = p.project_id
|
||||||
|
${whereClause}
|
||||||
|
GROUP BY p.project_name
|
||||||
|
HAVING hours > 0
|
||||||
|
ORDER BY hours DESC;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 3. 작업자별 집계 쿼리
|
||||||
|
const byWorkerSql = `
|
||||||
|
SELECT w.worker_name as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.project_id) as participants
|
||||||
|
FROM DailyWorkReports dwr
|
||||||
|
JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||||
|
${whereClause}
|
||||||
|
GROUP BY w.worker_name
|
||||||
|
HAVING hours > 0
|
||||||
|
ORDER BY hours DESC;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 4. 작업별 집계 쿼리
|
||||||
|
const byTaskSql = `
|
||||||
|
SELECT t.category as name, SUM(${workHoursCalc}) as hours, COUNT(DISTINCT dwr.worker_id) as participants
|
||||||
|
FROM DailyWorkReports dwr
|
||||||
|
JOIN Tasks t ON dwr.task_id = t.task_id
|
||||||
|
${whereClause}
|
||||||
|
GROUP BY t.category
|
||||||
|
HAVING hours > 0
|
||||||
|
ORDER BY hours DESC;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 5. 상세 내역 쿼리
|
||||||
|
const detailsSql = `
|
||||||
|
SELECT
|
||||||
|
dwr.report_date as date, p.project_name, w.worker_name,
|
||||||
|
t.category as task_category, dwr.work_details,
|
||||||
|
(${workHoursCalc}) as work_hours, dwr.memo
|
||||||
|
FROM DailyWorkReports dwr
|
||||||
|
JOIN Projects p ON dwr.project_id = p.project_id
|
||||||
|
JOIN Workers w ON dwr.worker_id = w.worker_id
|
||||||
|
JOIN Tasks t ON dwr.task_id = t.task_id
|
||||||
|
${whereClause}
|
||||||
|
HAVING work_hours > 0
|
||||||
|
ORDER BY dwr.report_date DESC;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 모든 쿼리를 병렬로 실행
|
||||||
|
const [
|
||||||
|
[summaryResult],
|
||||||
|
[byProject],
|
||||||
|
[byWorker],
|
||||||
|
[byTask],
|
||||||
|
[details]
|
||||||
|
] = await Promise.all([
|
||||||
|
db.query(summarySql, [startDate, endDate]),
|
||||||
|
db.query(byProjectSql, [startDate, endDate]),
|
||||||
|
db.query(byWorkerSql, [startDate, endDate]),
|
||||||
|
db.query(byTaskSql, [startDate, endDate]),
|
||||||
|
db.query(detailsSql, [startDate, endDate])
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: summaryResult[0],
|
||||||
|
byProject,
|
||||||
|
byWorker,
|
||||||
|
byTask,
|
||||||
|
details
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Model] 분석 데이터 조회 오류:', err);
|
||||||
|
throw new Error('데이터베이스에서 분석 데이터를 조회하는 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAnalysis
|
||||||
|
};
|
||||||
10
api.hyungi.net/routes/analysisRoutes.js
Normal file
10
api.hyungi.net/routes/analysisRoutes.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// /routes/analysisRoutes.js
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { getAnalysisData } = require('../controllers/analysisController');
|
||||||
|
const authMiddleware = require('../middlewares/authMiddleware'); // 인증 미들웨어 추가
|
||||||
|
|
||||||
|
// GET /api/analysis?startDate=...&endDate=...
|
||||||
|
router.get('/', authMiddleware, getAnalysisData);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
48
api.hyungi.net/services/analysisService.js
Normal file
48
api.hyungi.net/services/analysisService.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// /services/analysisService.js
|
||||||
|
const analysisModel = require('../models/analysisModel');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 기간별 프로젝트 분석 데이터를 조회하는 비즈니스 로직을 처리합니다.
|
||||||
|
* @param {string} startDate - 시작일 (YYYY-MM-DD)
|
||||||
|
* @param {string} endDate - 종료일 (YYYY-MM-DD)
|
||||||
|
* @returns {Promise<object>} - 가공된 분석 데이터
|
||||||
|
*/
|
||||||
|
const getAnalysisService = async (startDate, endDate) => {
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
throw new Error('분석을 위해 시작일과 종료일이 모두 필요합니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const analysisData = await analysisModel.getAnalysis(startDate, endDate);
|
||||||
|
|
||||||
|
// 모델에서 받은 데이터를 그대로 반환하거나, 필요 시 추가 가공을 할 수 있습니다.
|
||||||
|
// 예를 들어, 비율(percentage) 계산을 여기서 수행할 수 있습니다.
|
||||||
|
const { summary, byProject, byWorker, byTask, details } = analysisData;
|
||||||
|
const totalHours = summary.totalHours || 0;
|
||||||
|
|
||||||
|
const addPercentage = (item) => ({
|
||||||
|
...item,
|
||||||
|
hours: parseFloat(item.hours.toFixed(1)),
|
||||||
|
percentage: totalHours > 0 ? parseFloat((item.hours / totalHours * 100).toFixed(1)) : 0
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: {
|
||||||
|
...summary,
|
||||||
|
totalHours: parseFloat(totalHours.toFixed(1))
|
||||||
|
},
|
||||||
|
byProject: byProject.map(addPercentage),
|
||||||
|
byWorker: byWorker.map(addPercentage),
|
||||||
|
byTask: byTask.map(addPercentage),
|
||||||
|
details: details.map(d => ({...d, work_hours: parseFloat(d.work_hours.toFixed(1))})),
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Service] 분석 데이터 조회 중 오류 발생:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAnalysisService
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user