- 삭제된 DB 테이블들과 관련 코드 정리: * 12개 사용하지 않는 테이블 삭제 (activity_logs, CuttingPlan, DailyIssueReports 등) * 관련 모델, 컨트롤러, 라우트 파일들 삭제 * index.js에서 삭제된 라우트들 제거 - 웹 UI 페이지 정리: * 21개 사용하지 않는 페이지 삭제 * issue-reports 폴더 전체 삭제 * 모든 사용자 권한을 그룹장 대시보드로 통일 - 데이터베이스 스키마 정리: * v1 스키마로 통일 (daily_work_reports 테이블) * JSON 데이터 임포트 스크립트 구현 * 외래키 관계 정리 및 데이터 일관성 확보 - 통합 Docker Compose 설정: * 모든 서비스를 단일 docker-compose.yml로 통합 * 20000번대 포트 유지 * JWT 시크릿 및 환경변수 설정 - 문서화: * DATABASE_SCHEMA.md: 현재 DB 스키마 문서화 * DELETED_TABLES.md: 삭제된 테이블 목록 * DELETED_PAGES.md: 삭제된 페이지 목록
381 lines
12 KiB
JavaScript
381 lines
12 KiB
JavaScript
// controllers/workReportAnalysisController.js - 데일리 워크 레포트 분석 전용 컨트롤러
|
|
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
|
const { getDb } = require('../dbPool');
|
|
|
|
/**
|
|
* 📋 분석용 필터 데이터 조회 (프로젝트, 작업자, 작업유형 목록)
|
|
*/
|
|
const getAnalysisFilters = async (req, res) => {
|
|
try {
|
|
const db = await getDb();
|
|
|
|
// 프로젝트 목록
|
|
const [projects] = await db.query(`
|
|
SELECT DISTINCT p.project_id, p.project_name
|
|
FROM projects p
|
|
INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
|
|
ORDER BY p.project_name
|
|
`);
|
|
|
|
// 작업자 목록
|
|
const [workers] = await db.query(`
|
|
SELECT DISTINCT w.worker_id, w.worker_name
|
|
FROM workers w
|
|
INNER JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id
|
|
ORDER BY w.worker_name
|
|
`);
|
|
|
|
// 작업 유형 목록
|
|
const [workTypes] = await db.query(`
|
|
SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name
|
|
FROM work_types wt
|
|
INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id
|
|
ORDER BY wt.name
|
|
`);
|
|
|
|
// 날짜 범위 (최초/최신 데이터)
|
|
const [dateRange] = await db.query(`
|
|
SELECT
|
|
MIN(report_date) as min_date,
|
|
MAX(report_date) as max_date
|
|
FROM daily_work_reports
|
|
`);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
projects,
|
|
workers,
|
|
workTypes,
|
|
dateRange: dateRange[0]
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('필터 데이터 조회 오류:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: '필터 데이터 조회 중 오류가 발생했습니다.',
|
|
detail: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 📊 기간별 작업 분석 데이터 조회
|
|
*/
|
|
const getAnalyticsByPeriod = async (req, res) => {
|
|
try {
|
|
const { start_date, end_date, project_id, worker_id } = req.query;
|
|
|
|
if (!start_date || !end_date) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'start_date와 end_date가 필요합니다.',
|
|
example: 'start_date=2025-08-01&end_date=2025-08-31'
|
|
});
|
|
}
|
|
|
|
const db = await getDb();
|
|
|
|
// 기본 조건
|
|
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
|
|
let queryParams = [start_date, end_date];
|
|
|
|
// 프로젝트 필터
|
|
if (project_id) {
|
|
whereConditions.push('dwr.project_id = ?');
|
|
queryParams.push(project_id);
|
|
}
|
|
|
|
// 작업자 필터
|
|
if (worker_id) {
|
|
whereConditions.push('dwr.worker_id = ?');
|
|
queryParams.push(worker_id);
|
|
}
|
|
|
|
const whereClause = whereConditions.join(' AND ');
|
|
|
|
// 1. 전체 요약 통계 (에러 분석 포함)
|
|
const overallSql = `
|
|
SELECT
|
|
COUNT(*) as total_entries,
|
|
SUM(dwr.work_hours) as total_hours,
|
|
COUNT(DISTINCT dwr.worker_id) as unique_workers,
|
|
COUNT(DISTINCT dwr.project_id) as unique_projects,
|
|
COUNT(DISTINCT dwr.report_date) as working_days,
|
|
AVG(dwr.work_hours) as avg_hours_per_entry,
|
|
COUNT(DISTINCT dwr.created_by) as contributors,
|
|
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_entries,
|
|
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
|
FROM daily_work_reports dwr
|
|
WHERE ${whereClause}
|
|
`;
|
|
|
|
const [overallStats] = await db.query(overallSql, queryParams);
|
|
|
|
// 2. 일별 통계
|
|
const dailyStatsSql = `
|
|
SELECT
|
|
dwr.report_date,
|
|
SUM(dwr.work_hours) as daily_hours,
|
|
COUNT(*) as daily_entries,
|
|
COUNT(DISTINCT dwr.worker_id) as daily_workers
|
|
FROM daily_work_reports dwr
|
|
WHERE ${whereClause}
|
|
GROUP BY dwr.report_date
|
|
ORDER BY dwr.report_date ASC
|
|
`;
|
|
|
|
const [dailyStats] = await db.query(dailyStatsSql, queryParams);
|
|
|
|
// 2.5. 일별 에러 발생 통계
|
|
const dailyErrorStatsSql = `
|
|
SELECT
|
|
dwr.report_date,
|
|
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors,
|
|
COUNT(*) as daily_total,
|
|
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as daily_error_rate
|
|
FROM daily_work_reports dwr
|
|
WHERE ${whereClause}
|
|
GROUP BY dwr.report_date
|
|
ORDER BY dwr.report_date ASC
|
|
`;
|
|
|
|
const [dailyErrorStats] = await db.query(dailyErrorStatsSql, queryParams);
|
|
|
|
// 3. 에러 유형별 분석 (간단한 방식으로 수정)
|
|
const errorAnalysisSql = `
|
|
SELECT
|
|
et.id as error_type_id,
|
|
et.name as error_type_name,
|
|
COUNT(*) as error_count,
|
|
SUM(dwr.work_hours) as error_hours,
|
|
ROUND((COUNT(*) / (SELECT COUNT(*) FROM daily_work_reports WHERE error_type_id IS NOT NULL)) * 100, 2) as error_percentage
|
|
FROM daily_work_reports dwr
|
|
LEFT JOIN error_types et ON dwr.error_type_id = et.id
|
|
WHERE ${whereClause} AND dwr.error_type_id IS NOT NULL
|
|
GROUP BY et.id, et.name
|
|
ORDER BY error_count DESC
|
|
`;
|
|
|
|
const [errorAnalysis] = await db.query(errorAnalysisSql, queryParams);
|
|
|
|
// 4. 작업 유형별 분석
|
|
const workTypeAnalysisSql = `
|
|
SELECT
|
|
wt.id as work_type_id,
|
|
wt.name as work_type_name,
|
|
COUNT(*) as work_count,
|
|
SUM(dwr.work_hours) as total_hours,
|
|
AVG(dwr.work_hours) as avg_hours,
|
|
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
|
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
|
FROM daily_work_reports dwr
|
|
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
|
WHERE ${whereClause}
|
|
GROUP BY wt.id, wt.name
|
|
ORDER BY total_hours DESC
|
|
`;
|
|
|
|
const [workTypeAnalysis] = await db.query(workTypeAnalysisSql, queryParams);
|
|
|
|
// 5. 작업자별 성과 분석
|
|
const workerAnalysisSql = `
|
|
SELECT
|
|
w.worker_id,
|
|
w.worker_name,
|
|
COUNT(*) as total_entries,
|
|
SUM(dwr.work_hours) as total_hours,
|
|
AVG(dwr.work_hours) as avg_hours_per_entry,
|
|
COUNT(DISTINCT dwr.project_id) as projects_worked,
|
|
COUNT(DISTINCT dwr.report_date) as working_days,
|
|
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
|
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
|
FROM daily_work_reports dwr
|
|
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
|
WHERE ${whereClause}
|
|
GROUP BY w.worker_id, w.worker_name
|
|
ORDER BY total_hours DESC
|
|
`;
|
|
|
|
const [workerAnalysis] = await db.query(workerAnalysisSql, queryParams);
|
|
|
|
// 6. 프로젝트별 분석
|
|
const projectAnalysisSql = `
|
|
SELECT
|
|
p.project_id,
|
|
p.project_name,
|
|
COUNT(*) as total_entries,
|
|
SUM(dwr.work_hours) as total_hours,
|
|
COUNT(DISTINCT dwr.worker_id) as workers_count,
|
|
COUNT(DISTINCT dwr.report_date) as working_days,
|
|
AVG(dwr.work_hours) as avg_hours_per_entry,
|
|
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
|
|
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
|
|
FROM daily_work_reports dwr
|
|
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
|
WHERE ${whereClause}
|
|
GROUP BY p.project_id, p.project_name
|
|
ORDER BY total_hours DESC
|
|
`;
|
|
|
|
const [projectAnalysis] = await db.query(projectAnalysisSql, queryParams);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
summary: overallStats[0],
|
|
dailyStats,
|
|
dailyErrorStats,
|
|
errorAnalysis,
|
|
workTypeAnalysis,
|
|
workerAnalysis,
|
|
projectAnalysis,
|
|
period: { start_date, end_date },
|
|
filters: { project_id, worker_id }
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('기간별 분석 데이터 조회 오류:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: '기간별 분석 데이터 조회 중 오류가 발생했습니다.',
|
|
detail: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 📈 프로젝트별 상세 분석
|
|
*/
|
|
const getProjectAnalysis = async (req, res) => {
|
|
try {
|
|
const { start_date, end_date, project_id } = req.query;
|
|
|
|
if (!start_date || !end_date) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'start_date와 end_date가 필요합니다.'
|
|
});
|
|
}
|
|
|
|
const db = await getDb();
|
|
|
|
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
|
|
let queryParams = [start_date, end_date];
|
|
|
|
if (project_id) {
|
|
whereConditions.push('dwr.project_id = ?');
|
|
queryParams.push(project_id);
|
|
}
|
|
|
|
const whereClause = whereConditions.join(' AND ');
|
|
|
|
// 프로젝트별 통계
|
|
const projectStatsSql = `
|
|
SELECT
|
|
dwr.project_id,
|
|
p.project_name,
|
|
SUM(dwr.work_hours) as total_hours,
|
|
COUNT(*) as total_entries,
|
|
COUNT(DISTINCT dwr.worker_id) as workers_count,
|
|
COUNT(DISTINCT dwr.report_date) as working_days,
|
|
AVG(dwr.work_hours) as avg_hours_per_entry
|
|
FROM daily_work_reports dwr
|
|
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
|
WHERE ${whereClause}
|
|
GROUP BY dwr.project_id
|
|
ORDER BY total_hours DESC
|
|
`;
|
|
|
|
const [projectStats] = await db.query(projectStatsSql, queryParams);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
projectStats,
|
|
period: { start_date, end_date }
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('프로젝트별 분석 데이터 조회 오류:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: '프로젝트별 분석 데이터 조회 중 오류가 발생했습니다.',
|
|
detail: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 👤 작업자별 상세 분석
|
|
*/
|
|
const getWorkerAnalysis = async (req, res) => {
|
|
try {
|
|
const { start_date, end_date, worker_id } = req.query;
|
|
|
|
if (!start_date || !end_date) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'start_date와 end_date가 필요합니다.'
|
|
});
|
|
}
|
|
|
|
const db = await getDb();
|
|
|
|
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
|
|
let queryParams = [start_date, end_date];
|
|
|
|
if (worker_id) {
|
|
whereConditions.push('dwr.worker_id = ?');
|
|
queryParams.push(worker_id);
|
|
}
|
|
|
|
const whereClause = whereConditions.join(' AND ');
|
|
|
|
// 작업자별 통계
|
|
const workerStatsSql = `
|
|
SELECT
|
|
dwr.worker_id,
|
|
w.worker_name,
|
|
SUM(dwr.work_hours) as total_hours,
|
|
COUNT(*) as total_entries,
|
|
COUNT(DISTINCT dwr.project_id) as projects_worked,
|
|
COUNT(DISTINCT dwr.report_date) as working_days,
|
|
AVG(dwr.work_hours) as avg_hours_per_entry
|
|
FROM daily_work_reports dwr
|
|
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
|
WHERE ${whereClause}
|
|
GROUP BY dwr.worker_id
|
|
ORDER BY total_hours DESC
|
|
`;
|
|
|
|
const [workerStats] = await db.query(workerStatsSql, queryParams);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
workerStats,
|
|
period: { start_date, end_date }
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('작업자별 분석 데이터 조회 오류:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: '작업자별 분석 데이터 조회 중 오류가 발생했습니다.',
|
|
detail: error.message
|
|
});
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getAnalysisFilters,
|
|
getAnalyticsByPeriod,
|
|
getProjectAnalysis,
|
|
getWorkerAnalysis
|
|
}; |