feat: 데이터베이스 및 웹 UI 대규모 리팩토링
- 삭제된 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: 삭제된 페이지 목록
This commit is contained in:
@@ -19,29 +19,9 @@ const login = async (req, res) => {
|
||||
return res.status(result.status || 400).json({ error: result.error });
|
||||
}
|
||||
|
||||
// 로그인 성공 후, 역할에 따라 리디렉션 URL을 결정
|
||||
// 로그인 성공 후, 모든 권한을 그룹장 대시보드로 통일
|
||||
const user = result.data.user;
|
||||
let redirectUrl;
|
||||
|
||||
switch (user.role) {
|
||||
case 'system': // 시스템 계정 전용 대시보드
|
||||
redirectUrl = '/pages/dashboard/system.html';
|
||||
break;
|
||||
case 'admin':
|
||||
redirectUrl = '/pages/dashboard/admin.html';
|
||||
break;
|
||||
case 'leader':
|
||||
redirectUrl = '/pages/dashboard/group-leader.html';
|
||||
break;
|
||||
case 'support':
|
||||
// 예시: 지원팀 대시보드가 있다면
|
||||
// redirectUrl = '/pages/dashboard/support.html';
|
||||
// 없다면 일반 사용자 대시보드로
|
||||
redirectUrl = '/pages/dashboard/user.html';
|
||||
break;
|
||||
default:
|
||||
redirectUrl = '/pages/dashboard/user.html';
|
||||
}
|
||||
let redirectUrl = '/pages/dashboard/group-leader.html'; // 모든 사용자를 그룹장 대시보드로 리다이렉트
|
||||
|
||||
// 최종 응답에 redirectUrl을 포함하여 전달
|
||||
res.json({
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
// controllers/cuttingPlanController.js
|
||||
const cuttingPlanModel = require('../models/cuttingPlanModel');
|
||||
|
||||
// 1. 생성
|
||||
exports.createCuttingPlan = async (req, res) => {
|
||||
try {
|
||||
const planData = req.body;
|
||||
const lastID = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.create(planData, (err, id) => {
|
||||
if (err) return reject(err);
|
||||
resolve(id);
|
||||
});
|
||||
});
|
||||
res.json({ success: true, cutting_plan_id: lastID });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllCuttingPlans = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.getAll((err, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getCuttingPlanById = async (req, res) => {
|
||||
try {
|
||||
const cutting_plan_id = parseInt(req.params.cutting_plan_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.getById(cutting_plan_id, (err, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'CuttingPlan not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateCuttingPlan = async (req, res) => {
|
||||
try {
|
||||
const cutting_plan_id = parseInt(req.params.cutting_plan_id, 10);
|
||||
const planData = { ...req.body, cutting_plan_id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.update(planData, (err, count) => {
|
||||
if (err) return reject(err);
|
||||
resolve(count);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeCuttingPlan = async (req, res) => {
|
||||
try {
|
||||
const cutting_plan_id = parseInt(req.params.cutting_plan_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
cuttingPlanModel.remove(cutting_plan_id, (err, count) => {
|
||||
if (err) return reject(err);
|
||||
resolve(count);
|
||||
});
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'CuttingPlan not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
// controllers/equipmentListController.js
|
||||
const equipmentListModel = require('../models/equipmentListModel');
|
||||
|
||||
// 1. 등록
|
||||
exports.createEquipment = async (req, res) => {
|
||||
try {
|
||||
const equipmentData = req.body;
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.create(equipmentData, (err, insertId) =>
|
||||
err ? reject(err) : resolve(insertId)
|
||||
);
|
||||
});
|
||||
res.json({ success: true, equipment_id: id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllEquipment = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.getAll((err, data) =>
|
||||
err ? reject(err) : resolve(data)
|
||||
);
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getEquipmentById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.equipment_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.getById(id, (err, data) =>
|
||||
err ? reject(err) : resolve(data)
|
||||
);
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Equipment not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateEquipment = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.equipment_id, 10);
|
||||
const data = { ...req.body, equipment_id: id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.update(data, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeEquipment = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.equipment_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
equipmentListModel.remove(id, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Equipment not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
// controllers/factoryInfoController.js
|
||||
const factoryInfoModel = require('../models/factoryInfoModel');
|
||||
|
||||
// 1. 공장 정보 생성
|
||||
exports.createFactoryInfo = async (req, res) => {
|
||||
try {
|
||||
const factoryData = req.body;
|
||||
const id = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.create(factoryData, (err, insertId) =>
|
||||
err ? reject(err) : resolve(insertId)
|
||||
);
|
||||
});
|
||||
res.json({ success: true, factory_id: id });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 공장 정보 조회
|
||||
exports.getAllFactoryInfo = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.getAll((err, data) =>
|
||||
err ? reject(err) : resolve(data)
|
||||
);
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getFactoryInfoById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.factory_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.getById(id, (err, data) =>
|
||||
err ? reject(err) : resolve(data)
|
||||
);
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'FactoryInfo not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateFactoryInfo = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.factory_id, 10);
|
||||
const data = { ...req.body, factory_id: id };
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.update(data, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeFactoryInfo = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.factory_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
factoryInfoModel.remove(id, (err, affectedRows) =>
|
||||
err ? reject(err) : resolve(affectedRows)
|
||||
);
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'FactoryInfo not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
// controllers/pingController.js
|
||||
const { getDb } = require('../dbPool');
|
||||
const pingModel = require('../models/pingModel');
|
||||
|
||||
exports.ping = async (req, res) => {
|
||||
const data = pingModel.ping();
|
||||
try {
|
||||
// DB 연결 테스트
|
||||
const db = await getDb();
|
||||
await db.query('SELECT 1');
|
||||
return res.json({
|
||||
success: true,
|
||||
...data,
|
||||
db: 'ok'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[PING ERROR]', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'db error',
|
||||
timestamp: data.timestamp,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// ✅ 전체 스펙 목록 (프론트 드롭다운용 label 포함)
|
||||
exports.getAll = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT spec_id, material, diameter_in, schedule
|
||||
FROM PipeSpecs
|
||||
ORDER BY material, diameter_in
|
||||
`);
|
||||
|
||||
const result = rows.map(row => ({
|
||||
spec_id: row.spec_id,
|
||||
label: `${row.material} / ${row.diameter_in} / ${row.schedule}`
|
||||
}));
|
||||
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
console.error('[getAll 오류]', err);
|
||||
res.status(500).json({ error: '파이프 스펙 전체 조회 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 등록
|
||||
exports.create = async (req, res) => {
|
||||
try {
|
||||
const { material, diameter_in, schedule } = req.body;
|
||||
if (!material || !diameter_in || !schedule) {
|
||||
return res.status(400).json({ error: '모든 항목이 필요합니다.' });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 중복 체크
|
||||
const [existing] = await db.query(
|
||||
`SELECT * FROM PipeSpecs WHERE material = ? AND diameter_in = ? AND schedule = ?`,
|
||||
[material.trim(), diameter_in.trim(), schedule.trim()]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return res.status(409).json({ error: '이미 등록된 스펙입니다.' });
|
||||
}
|
||||
|
||||
await db.query(
|
||||
`INSERT INTO PipeSpecs (material, diameter_in, schedule) VALUES (?, ?, ?)`,
|
||||
[material.trim(), diameter_in.trim(), schedule.trim()]
|
||||
);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[create 오류]', err);
|
||||
res.status(500).json({ error: '파이프 스펙 등록 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 삭제
|
||||
exports.remove = async (req, res) => {
|
||||
const { spec_id } = req.params;
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM PipeSpecs WHERE spec_id = ?`,
|
||||
[spec_id]
|
||||
);
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({ error: '해당 스펙이 존재하지 않습니다.' });
|
||||
}
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[remove 오류]', err);
|
||||
res.status(500).json({ error: '파이프 스펙 삭제 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 재질 목록
|
||||
exports.getMaterials = async (req, res) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT DISTINCT material FROM PipeSpecs ORDER BY material`
|
||||
);
|
||||
res.json(rows.map(row => row.material));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: '재질 목록 조회 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 직경 목록 (material 기준)
|
||||
exports.getDiameters = async (req, res) => {
|
||||
const { material } = req.query;
|
||||
if (!material) {
|
||||
return res.status(400).json({ error: 'material 파라미터가 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT DISTINCT diameter_in FROM PipeSpecs WHERE material = ? ORDER BY diameter_in`,
|
||||
[material]
|
||||
);
|
||||
res.json(rows.map(row => row.diameter_in));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: '직경 목록 조회 실패' });
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 스케줄 목록 (material + 직경 기준)
|
||||
exports.getSchedules = async (req, res) => {
|
||||
const { material, diameter_in } = req.query;
|
||||
if (!material || !diameter_in) {
|
||||
return res.status(400).json({ error: 'material과 diameter_in이 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT DISTINCT schedule FROM PipeSpecs
|
||||
WHERE material = ? AND diameter_in = ?
|
||||
ORDER BY schedule`,
|
||||
[material, diameter_in]
|
||||
);
|
||||
res.json(rows.map(row => row.schedule));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: '스케줄 목록 조회 실패' });
|
||||
}
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
const processModel = require('../models/processModel');
|
||||
const projectModel = require('../models/projectModel');
|
||||
|
||||
// 1. 공정 등록
|
||||
exports.createProcess = async (req, res) => {
|
||||
try {
|
||||
const processData = req.body;
|
||||
|
||||
if (!processData.process_end) {
|
||||
const project = await new Promise((resolve, reject) => {
|
||||
projectModel.getById(processData.project_id, (err, row) => {
|
||||
if (err) return reject(err);
|
||||
if (!row) return reject({ status: 404, message: 'Project not found' });
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
processData.process_end = project.due_date;
|
||||
}
|
||||
|
||||
const lastID = await new Promise((resolve, reject) => {
|
||||
processModel.create(processData, (err, id) => (err ? reject(err) : resolve(id)));
|
||||
});
|
||||
|
||||
res.json({ success: true, process_id: lastID });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
res.status(status).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
exports.getAllProcesses = async (req, res) => {
|
||||
try {
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
processModel.getAll((err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
exports.getProcessById = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.process_id, 10);
|
||||
const row = await new Promise((resolve, reject) => {
|
||||
processModel.getById(id, (err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
if (!row) return res.status(404).json({ error: 'Process not found' });
|
||||
res.json(row);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. 수정
|
||||
exports.updateProcess = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.process_id, 10);
|
||||
const processData = { ...req.body, process_id: id };
|
||||
|
||||
if (!processData.process_end) {
|
||||
const project = await new Promise((resolve, reject) => {
|
||||
projectModel.getById(processData.project_id, (err, row) => {
|
||||
if (err) return reject(err);
|
||||
if (!row) return reject({ status: 404, message: 'Project not found' });
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
processData.process_end = project.due_date;
|
||||
}
|
||||
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
processModel.update(processData, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
|
||||
if (changes === 0) return res.status(404).json({ error: 'No changes or not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
const status = err.status || 500;
|
||||
res.status(status).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 삭제
|
||||
exports.removeProcess = async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.process_id, 10);
|
||||
const changes = await new Promise((resolve, reject) => {
|
||||
processModel.remove(id, (err, ch) => (err ? reject(err) : resolve(ch)));
|
||||
});
|
||||
if (changes === 0) return res.status(404).json({ error: 'Process not found' });
|
||||
res.json({ success: true, changes });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message || String(err) });
|
||||
}
|
||||
};
|
||||
@@ -16,6 +16,7 @@ class WorkAnalysisController {
|
||||
this.getErrorAnalysis = this.getErrorAnalysis.bind(this);
|
||||
this.getMonthlyComparison = this.getMonthlyComparison.bind(this);
|
||||
this.getWorkerSpecialization = this.getWorkerSpecialization.bind(this);
|
||||
this.getProjectWorkTypeAnalysis = this.getProjectWorkTypeAnalysis.bind(this);
|
||||
}
|
||||
|
||||
// 날짜 유효성 검사
|
||||
@@ -367,6 +368,142 @@ class WorkAnalysisController {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트별-작업별 시간 분석 (총시간, 정규시간, 에러시간)
|
||||
async getProjectWorkTypeAnalysis(req, res) {
|
||||
try {
|
||||
const { start, end } = req.query;
|
||||
this.validateDateRange(start, end);
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
// 먼저 데이터 존재 여부 확인
|
||||
const testQuery = `
|
||||
SELECT
|
||||
COUNT(*) as total_count,
|
||||
MIN(report_date) as min_date,
|
||||
MAX(report_date) as max_date,
|
||||
SUM(work_hours) as total_hours
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const testResults = await db.query(testQuery, [start, end]);
|
||||
console.log('📊 데이터 확인:', testResults[0]);
|
||||
|
||||
// 프로젝트별-작업별 시간 분석 쿼리 (간단한 버전으로 테스트)
|
||||
const query = `
|
||||
SELECT
|
||||
COALESCE(p.project_id, 0) as project_id,
|
||||
COALESCE(p.project_name, 'Unknown Project') as project_name,
|
||||
COALESCE(p.job_no, 'N/A') as job_no,
|
||||
dwr.work_type_id,
|
||||
CONCAT('Work Type ', dwr.work_type_id) as work_type_name,
|
||||
|
||||
-- 총 시간
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
|
||||
-- 정규 시간 (work_status_id = 1)
|
||||
SUM(CASE WHEN dwr.work_status_id = 1 THEN dwr.work_hours ELSE 0 END) as regular_hours,
|
||||
|
||||
-- 에러 시간 (work_status_id = 2)
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) as error_hours,
|
||||
|
||||
-- 작업 건수
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 1 THEN 1 END) as regular_reports,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_reports,
|
||||
|
||||
-- 에러율 계산
|
||||
ROUND(
|
||||
(SUM(CASE WHEN dwr.work_status_id = 2 THEN dwr.work_hours ELSE 0 END) /
|
||||
SUM(dwr.work_hours)) * 100, 2
|
||||
) as error_rate_percent
|
||||
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY p.project_id, p.project_name, p.job_no, dwr.work_type_id
|
||||
ORDER BY p.project_name, dwr.work_type_id
|
||||
`;
|
||||
|
||||
const results = await db.query(query, [start, end]);
|
||||
|
||||
// 데이터를 프로젝트별로 그룹화
|
||||
const groupedData = {};
|
||||
|
||||
results.forEach(row => {
|
||||
const projectKey = `${row.project_id}_${row.project_name}`;
|
||||
|
||||
if (!groupedData[projectKey]) {
|
||||
groupedData[projectKey] = {
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name,
|
||||
job_no: row.job_no,
|
||||
total_project_hours: 0,
|
||||
total_regular_hours: 0,
|
||||
total_error_hours: 0,
|
||||
work_types: []
|
||||
};
|
||||
}
|
||||
|
||||
// 프로젝트 총계 누적
|
||||
groupedData[projectKey].total_project_hours += parseFloat(row.total_hours);
|
||||
groupedData[projectKey].total_regular_hours += parseFloat(row.regular_hours);
|
||||
groupedData[projectKey].total_error_hours += parseFloat(row.error_hours);
|
||||
|
||||
// 작업 유형별 데이터 추가
|
||||
groupedData[projectKey].work_types.push({
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name,
|
||||
total_hours: parseFloat(row.total_hours),
|
||||
regular_hours: parseFloat(row.regular_hours),
|
||||
error_hours: parseFloat(row.error_hours),
|
||||
total_reports: row.total_reports,
|
||||
regular_reports: row.regular_reports,
|
||||
error_reports: row.error_reports,
|
||||
error_rate_percent: parseFloat(row.error_rate_percent) || 0
|
||||
});
|
||||
});
|
||||
|
||||
// 프로젝트별 에러율 계산
|
||||
Object.values(groupedData).forEach(project => {
|
||||
project.project_error_rate = project.total_project_hours > 0
|
||||
? Math.round((project.total_error_hours / project.total_project_hours) * 100 * 100) / 100
|
||||
: 0;
|
||||
});
|
||||
|
||||
// 전체 요약 통계
|
||||
const totalStats = {
|
||||
total_projects: Object.keys(groupedData).length,
|
||||
total_work_types: new Set(results.map(r => r.work_type_id)).size,
|
||||
grand_total_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_project_hours, 0),
|
||||
grand_regular_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_regular_hours, 0),
|
||||
grand_error_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_error_hours, 0)
|
||||
};
|
||||
|
||||
totalStats.grand_error_rate = totalStats.grand_total_hours > 0
|
||||
? Math.round((totalStats.grand_error_hours / totalStats.grand_total_hours) * 100 * 100) / 100
|
||||
: 0;
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: {
|
||||
summary: totalStats,
|
||||
projects: Object.values(groupedData),
|
||||
period: { start, end }
|
||||
},
|
||||
message: '프로젝트별-작업별 시간 분석 완료'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트별-작업별 시간 분석 오류:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WorkAnalysisController();
|
||||
381
api.hyungi.net/controllers/workReportAnalysisController.js
Normal file
381
api.hyungi.net/controllers/workReportAnalysisController.js
Normal file
@@ -0,0 +1,381 @@
|
||||
// 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
|
||||
};
|
||||
Reference in New Issue
Block a user