From 7c6940307e1599183b53f15fc2c613acc79db233 Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 28 Jul 2025 15:13:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EC=84=9D=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프론트엔드의 데이터 가공 로직을 백엔드로 이전하여 성능 개선 - 단일 API 호출로 모든 집계 데이터를 반환하도록 C-S-M 아키텍처 구현 --- .../controllers/analysisController.js | 22 ++++ api.hyungi.net/index.js | 2 + api.hyungi.net/models/analysisModel.js | 115 ++++++++++++++++++ api.hyungi.net/routes/analysisRoutes.js | 10 ++ api.hyungi.net/services/analysisService.js | 48 ++++++++ 5 files changed, 197 insertions(+) create mode 100644 api.hyungi.net/controllers/analysisController.js create mode 100644 api.hyungi.net/models/analysisModel.js create mode 100644 api.hyungi.net/routes/analysisRoutes.js create mode 100644 api.hyungi.net/services/analysisService.js diff --git a/api.hyungi.net/controllers/analysisController.js b/api.hyungi.net/controllers/analysisController.js new file mode 100644 index 0000000..474072b --- /dev/null +++ b/api.hyungi.net/controllers/analysisController.js @@ -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, +}; \ No newline at end of file diff --git a/api.hyungi.net/index.js b/api.hyungi.net/index.js index 0fe9d2c..ac29d6d 100644 --- a/api.hyungi.net/index.js +++ b/api.hyungi.net/index.js @@ -135,6 +135,7 @@ const healthRoutes = require('./routes/healthRoutes'); const pipeSpecRoutes = require('./routes/pipeSpecRoutes'); const dailyWorkReportRoutes = require('./routes/dailyWorkReportRoutes'); const workAnalysisRoutes = require('./routes/workAnalysisRoutes'); +const analysisRoutes = require('./routes/analysisRoutes'); // 새로운 분석 라우트 // 🔒 인증 미들웨어 가져오기 const { verifyToken } = require('./middlewares/authMiddleware'); @@ -243,6 +244,7 @@ app.use('/api/issue-types', issueTypeRoutes); app.use('/api/workers', workerRoutes); app.use('/api/daily-work-reports', dailyWorkReportRoutes); app.use('/api/work-analysis', workAnalysisRoutes); +app.use('/api/analysis', analysisRoutes); // 새로운 분석 라우트 등록 // 📊 리포트 및 분석 app.use('/api/workreports', workReportRoutes); diff --git a/api.hyungi.net/models/analysisModel.js b/api.hyungi.net/models/analysisModel.js new file mode 100644 index 0000000..671bfce --- /dev/null +++ b/api.hyungi.net/models/analysisModel.js @@ -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} - 요약, 프로젝트별, 작업자별, 작업별 집계 데이터 및 상세 내역 + */ +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 +}; \ No newline at end of file diff --git a/api.hyungi.net/routes/analysisRoutes.js b/api.hyungi.net/routes/analysisRoutes.js new file mode 100644 index 0000000..a9ca63b --- /dev/null +++ b/api.hyungi.net/routes/analysisRoutes.js @@ -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; \ No newline at end of file diff --git a/api.hyungi.net/services/analysisService.js b/api.hyungi.net/services/analysisService.js new file mode 100644 index 0000000..f2a1682 --- /dev/null +++ b/api.hyungi.net/services/analysisService.js @@ -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} - 가공된 분석 데이터 + */ +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 +}; \ No newline at end of file