refactor: Phase 1 - 긴급 보안 및 중복 제거
## 🚨 보안 강화 - 하드코딩된 비밀번호를 환경변수로 전환 - .env.example 생성 및 보안 가이드 추가 - docker-compose.yml 환경변수 적용 - README.md에서 실제 비밀번호 제거 ## 🗑️ 중복 제거 - synology_deployment/ 디렉토리 제거 (268MB) - synology_deployment*.tar.gz 아카이브 제거 (234MB) - 총 502MB의 중복 파일 삭제 ## 🧹 백업 파일 정리 - *.backup 파일 제거 (10개) - *복사본* 파일 제거 - *이전* 파일 제거 - json(백업)/ 디렉토리 제거 ## 📋 .gitignore 업데이트 - 백업 파일 패턴 추가 - 보안 파일 제외 (.env, *.pem, *.key) - 임시 파일 제외 (*.tmp, *.new) - 빌드 아티팩트 제외 (*.tar.gz) ## 📚 문서화 - docs/ 디렉토리 구조 생성 - 리팩토링 분석 및 계획 문서 작성 - 코딩 스타일 가이드 작성 - 개발 환경 설정 가이드 작성 - 시스템 아키텍처 문서 작성 ## 변경된 파일 - .env.example (신규) - .gitignore (업데이트) - docker-compose.yml (환경변수 적용) - README.md (보안 정보 제거) - docs/* (신규 문서 7개) ## 보안 개선 효과 ✅ 비밀번호 노출 위험 제거 ✅ Git 히스토리에서 민감 정보 분리 ✅ 환경별 설정 분리 가능 ✅ 배포 보안 강화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,750 +0,0 @@
|
||||
// controllers/dailyWorkReportController.js - 누적입력 방식 + 모든 기존 기능 포함
|
||||
const dailyWorkReportModel = require('../models/dailyWorkReportModel');
|
||||
|
||||
/**
|
||||
* 📝 작업보고서 생성 (누적 방식 - 덮어쓰기 없음!)
|
||||
*/
|
||||
const createDailyWorkReport = (req, res) => {
|
||||
const { report_date, worker_id, work_entries } = req.body;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
const created_by_name = req.user?.name || req.user?.username || '알 수 없는 사용자';
|
||||
|
||||
// 1. 기본 유효성 검사
|
||||
if (!report_date || !worker_id || !work_entries) {
|
||||
return res.status(400).json({
|
||||
error: '필수 필드가 누락되었습니다.',
|
||||
required: ['report_date', 'worker_id', 'work_entries'],
|
||||
received: {
|
||||
report_date: !!report_date,
|
||||
worker_id: !!worker_id,
|
||||
work_entries: !!work_entries
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Array.isArray(work_entries) || work_entries.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: '최소 하나의 작업 항목이 필요합니다.',
|
||||
received_entries: work_entries?.length || 0
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 작업 항목 유효성 검사
|
||||
for (let i = 0; i < work_entries.length; i++) {
|
||||
const entry = work_entries[i];
|
||||
const requiredFields = ['project_id', 'work_type_id', 'work_status_id', 'work_hours'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}의 ${field}가 누락되었습니다.`,
|
||||
entry_index: i,
|
||||
missing_field: field
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 상태인 경우 에러 타입 필수
|
||||
if (entry.work_status_id === 2 && (!entry.error_type_id)) {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}이 에러 상태인 경우 error_type_id가 필요합니다.`,
|
||||
entry_index: i
|
||||
});
|
||||
}
|
||||
|
||||
// 시간 유효성 검사
|
||||
const hours = parseFloat(entry.work_hours);
|
||||
if (isNaN(hours) || hours < 0 || hours > 24) {
|
||||
return res.status(400).json({
|
||||
error: `작업 항목 ${i + 1}의 작업시간이 유효하지 않습니다. (0-24시간)`,
|
||||
entry_index: i,
|
||||
received_hours: entry.work_hours
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 총 시간 계산
|
||||
const total_hours = work_entries.reduce((sum, entry) => sum + (parseFloat(entry.work_hours) || 0), 0);
|
||||
|
||||
// 4. 요청 데이터 구성
|
||||
const reportData = {
|
||||
report_date,
|
||||
worker_id: parseInt(worker_id),
|
||||
work_entries,
|
||||
created_by,
|
||||
created_by_name,
|
||||
total_hours,
|
||||
is_update: false
|
||||
};
|
||||
|
||||
console.log('📝 작업보고서 누적 추가 요청:', {
|
||||
date: report_date,
|
||||
worker: worker_id,
|
||||
creator: created_by_name,
|
||||
creator_id: created_by,
|
||||
entries: work_entries.length,
|
||||
total_hours
|
||||
});
|
||||
|
||||
// 5. 누적 추가 실행 (덮어쓰기 없음!)
|
||||
dailyWorkReportModel.createDailyReport(reportData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 생성 중 오류가 발생했습니다.',
|
||||
details: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 작업보고서 누적 추가 성공:', result);
|
||||
res.status(201).json({
|
||||
message: '작업보고서가 성공적으로 누적 추가되었습니다.',
|
||||
report_date,
|
||||
worker_id,
|
||||
created_by: created_by_name,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getAccumulatedReports = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 누적 현황 조회 결과: ${data.length}개`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
total_entries: data.length,
|
||||
accumulated_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 기여자별 요약 조회 (새로운 기능)
|
||||
*/
|
||||
const getContributorsSummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 기여자별 요약 조회: date=${date}, worker_id=${worker_id}`);
|
||||
|
||||
dailyWorkReportModel.getContributorsByDate(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('기여자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '기여자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0);
|
||||
|
||||
console.log(`📊 기여자별 요약: ${data.length}명, 총 ${totalHours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
contributors: data,
|
||||
total_contributors: data.length,
|
||||
grand_total_hours: totalHours,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 개인 누적 현황 조회 (새로운 기능)
|
||||
*/
|
||||
const getMyAccumulatedData = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!date || !worker_id) {
|
||||
return res.status(400).json({
|
||||
error: 'date와 worker_id가 필요합니다.',
|
||||
example: 'date=2024-06-16&worker_id=1'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적 현황 조회: date=${date}, worker_id=${worker_id}, created_by=${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('개인 누적 현황 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '개인 누적 현황 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 개인 누적: ${data.my_entry_count}개 항목, ${data.my_total_hours}시간`);
|
||||
res.json({
|
||||
date,
|
||||
worker_id,
|
||||
created_by,
|
||||
my_data: data,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 개별 항목 삭제 (본인 작성분만 - 새로운 기능)
|
||||
*/
|
||||
const removeMyEntry = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 개별 항목 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeSpecificEntry(id, deleted_by, (err, result) => {
|
||||
if (err) {
|
||||
console.error('개별 항목 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '항목 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 개별 항목 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '항목이 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString(),
|
||||
...result
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 작업보고서 조회 (쿼리 파라미터 기반 - 작성자별 필터링 강화)
|
||||
*/
|
||||
const getDailyWorkReports = (req, res) => {
|
||||
const { date, worker_id, created_by: requested_created_by } = req.query;
|
||||
const current_user_id = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!current_user_id) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 일반 사용자는 자신이 작성한 것만 볼 수 있음
|
||||
const created_by = requested_created_by || current_user_id;
|
||||
|
||||
console.log('📊 작업보고서 조회 요청:', {
|
||||
date,
|
||||
worker_id,
|
||||
requested_created_by,
|
||||
current_user_id,
|
||||
final_created_by: created_by
|
||||
});
|
||||
|
||||
if (date && created_by) {
|
||||
// 날짜 + 작성자별 조회
|
||||
dailyWorkReportModel.getByDateAndCreator(date, created_by, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 날짜+작성자별 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
} else if (date && worker_id) {
|
||||
// 기존 방식: 날짜 + 작업자별 (하지만 작성자 필터링 추가)
|
||||
dailyWorkReportModel.getByDateAndWorker(date, worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 날짜+작업자별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
} else if (date) {
|
||||
// 날짜별 조회 (작성자 필터링)
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === current_user_id);
|
||||
console.log(`📊 날짜별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: '날짜(date) 파라미터가 필요합니다.',
|
||||
example: 'date=2024-06-16',
|
||||
optional: ['worker_id', 'created_by']
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 날짜별 작업보고서 조회 (경로 파라미터)
|
||||
*/
|
||||
const getDailyWorkReportsByDate = (req, res) => {
|
||||
const { date } = req.params;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 날짜별 조회 (경로): date=${date}, created_by=${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('날짜별 작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 본인이 작성한 것만 필터링
|
||||
const filteredData = data.filter(report => report.created_by === created_by);
|
||||
console.log(`📊 날짜별 조회 결과: 전체 ${data.length}개 → 필터링 후 ${filteredData.length}개`);
|
||||
res.json(filteredData);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔍 작업보고서 검색 (페이지네이션 포함)
|
||||
*/
|
||||
const searchWorkReports = (req, res) => {
|
||||
const { start_date, end_date, worker_id, project_id, work_status_id, page = 1, limit = 20 } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31',
|
||||
optional: ['worker_id', 'project_id', 'work_status_id', 'page', 'limit']
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const searchParams = {
|
||||
start_date,
|
||||
end_date,
|
||||
worker_id: worker_id ? parseInt(worker_id) : null,
|
||||
project_id: project_id ? parseInt(project_id) : null,
|
||||
work_status_id: work_status_id ? parseInt(work_status_id) : null,
|
||||
created_by, // 작성자 필터링 추가
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
};
|
||||
|
||||
console.log('🔍 작업보고서 검색 요청:', searchParams);
|
||||
|
||||
dailyWorkReportModel.searchWithDetails(searchParams, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 검색 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 검색 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🔍 검색 결과: ${data.reports?.length || 0}개 (전체: ${data.total || 0}개)`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📈 통계 조회 (작성자별 필터링)
|
||||
*/
|
||||
const getWorkReportStats = (req, res) => {
|
||||
const { start_date, end_date } = req.query;
|
||||
const created_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({
|
||||
error: 'start_date와 end_date가 필요합니다.',
|
||||
example: 'start_date=2024-01-01&end_date=2024-01-31'
|
||||
});
|
||||
}
|
||||
|
||||
if (!created_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📈 통계 조회: ${start_date} ~ ${end_date}, 요청자: ${created_by}`);
|
||||
|
||||
dailyWorkReportModel.getStatistics(start_date, end_date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('통계 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '통계 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
...data,
|
||||
metadata: {
|
||||
note: '현재는 전체 통계입니다. 개인별 통계는 추후 구현 예정',
|
||||
requested_by: created_by,
|
||||
period: `${start_date} ~ ${end_date}`,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📊 일일 근무 요약 조회
|
||||
*/
|
||||
const getDailySummary = (req, res) => {
|
||||
const { date, worker_id } = req.query;
|
||||
|
||||
if (date) {
|
||||
console.log(`📊 일일 요약 조회: date=${date}`);
|
||||
dailyWorkReportModel.getSummaryByDate(date, (err, data) => {
|
||||
if (err) {
|
||||
console.error('일일 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '일일 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else if (worker_id) {
|
||||
console.log(`📊 작업자별 요약 조회: worker_id=${worker_id}`);
|
||||
dailyWorkReportModel.getSummaryByWorker(worker_id, (err, data) => {
|
||||
if (err) {
|
||||
console.error('작업자별 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업자별 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
res.json(data);
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
error: 'date 또는 worker_id 파라미터가 필요합니다.',
|
||||
examples: [
|
||||
'date=2024-06-16',
|
||||
'worker_id=1'
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 📅 월간 요약 조회
|
||||
*/
|
||||
const getMonthlySummary = (req, res) => {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).json({
|
||||
error: 'year와 month가 필요합니다.',
|
||||
example: 'year=2024&month=01',
|
||||
note: 'month는 01, 02, ..., 12 형식으로 입력하세요.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📅 월간 요약 조회: ${year}-${month}`);
|
||||
|
||||
dailyWorkReportModel.getMonthlySummary(year, month, (err, data) => {
|
||||
if (err) {
|
||||
console.error('월간 요약 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '월간 요약 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
year: parseInt(year),
|
||||
month: parseInt(month),
|
||||
summary: data,
|
||||
total_entries: data.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* ✏️ 작업보고서 수정
|
||||
*/
|
||||
const updateWorkReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
const updated_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!updated_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
updateData.updated_by = updated_by;
|
||||
|
||||
console.log(`✏️ 작업보고서 수정 요청: id=${id}, 수정자=${updated_by}`);
|
||||
|
||||
dailyWorkReportModel.updateById(id, updateData, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 수정 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '수정할 작업보고서를 찾을 수 없습니다.',
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 작업보고서 수정 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '작업보고서가 성공적으로 수정되었습니다.',
|
||||
id: id,
|
||||
affected_rows: affectedRows,
|
||||
updated_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 특정 작업보고서 삭제
|
||||
*/
|
||||
const removeDailyWorkReport = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 작업보고서 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeById(id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 작업보고서 삭제 완료: id=${id}`);
|
||||
res.json({
|
||||
message: '작업보고서가 성공적으로 삭제되었습니다.',
|
||||
id: id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 🗑️ 작업자의 특정 날짜 전체 삭제
|
||||
*/
|
||||
const removeDailyWorkReportByDateAndWorker = (req, res) => {
|
||||
const { date, worker_id } = req.params;
|
||||
const deleted_by = req.user?.user_id || req.user?.id;
|
||||
|
||||
if (!deleted_by) {
|
||||
return res.status(401).json({
|
||||
error: '사용자 인증 정보가 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
|
||||
|
||||
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
|
||||
if (err) {
|
||||
console.error('작업보고서 전체 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업보고서 삭제 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
error: '삭제할 작업보고서를 찾을 수 없습니다.',
|
||||
date: date,
|
||||
worker_id: worker_id
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ 날짜+작업자별 전체 삭제 완료: ${affectedRows}개`);
|
||||
res.json({
|
||||
message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
|
||||
date,
|
||||
worker_id,
|
||||
affected_rows: affectedRows,
|
||||
deleted_by,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 📋 마스터 데이터 조회 함수들
|
||||
*/
|
||||
const getWorkTypes = (req, res) => {
|
||||
console.log('📋 작업 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('작업 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '작업 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 작업 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getWorkStatusTypes = (req, res) => {
|
||||
console.log('📋 업무 상태 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllWorkStatusTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('업무 상태 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '업무 상태 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 업무 상태 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
const getErrorTypes = (req, res) => {
|
||||
console.log('📋 에러 유형 조회 요청');
|
||||
dailyWorkReportModel.getAllErrorTypes((err, data) => {
|
||||
if (err) {
|
||||
console.error('에러 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
error: '에러 유형 조회 중 오류가 발생했습니다.',
|
||||
details: err.message
|
||||
});
|
||||
}
|
||||
console.log(`📋 에러 유형 조회 결과: ${data.length}개`);
|
||||
res.json(data);
|
||||
});
|
||||
};
|
||||
|
||||
// 모든 컨트롤러 함수 내보내기 (기존 기능 + 누적 기능)
|
||||
module.exports = {
|
||||
// 📝 핵심 CRUD 함수들
|
||||
createDailyWorkReport, // 누적 추가 (덮어쓰기 없음)
|
||||
getDailyWorkReports, // 조회 (작성자별 필터링)
|
||||
getDailyWorkReportsByDate, // 날짜별 조회
|
||||
searchWorkReports, // 검색 (페이지네이션)
|
||||
updateWorkReport, // 수정
|
||||
removeDailyWorkReport, // 개별 삭제
|
||||
removeDailyWorkReportByDateAndWorker, // 전체 삭제
|
||||
|
||||
// 🔄 누적 관련 새로운 함수들
|
||||
getAccumulatedReports, // 누적 현황 조회
|
||||
getContributorsSummary, // 기여자별 요약
|
||||
getMyAccumulatedData, // 개인 누적 현황
|
||||
removeMyEntry, // 개별 항목 삭제 (본인 것만)
|
||||
|
||||
// 📊 요약 및 통계 함수들
|
||||
getDailySummary, // 일일 요약
|
||||
getMonthlySummary, // 월간 요약
|
||||
getWorkReportStats, // 통계
|
||||
|
||||
// 📋 마스터 데이터 함수들
|
||||
getWorkTypes, // 작업 유형 목록
|
||||
getWorkStatusTypes, // 업무 상태 유형 목록
|
||||
getErrorTypes // 에러 유형 목록
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10.9
|
||||
container_name: db_hyungi_net
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
- MYSQL_DATABASE=${DB_NAME}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./migrations:/docker-entrypoint-initdb.d # SQL 마이그레이션 자동 실행
|
||||
ports:
|
||||
- "3306:3306" # 개발 시 외부 접속용 (운영 시 제거)
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: api_hyungi_net
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy # DB가 준비된 후 시작
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3005}:3005"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
volumes:
|
||||
- ./public/img:/usr/src/app/public/img:ro
|
||||
- ./uploads:/usr/src/app/uploads
|
||||
- ./logs:/usr/src/app/logs # 로그 파일 저장
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin/phpmyadmin:latest
|
||||
container_name: pma_hyungi_net
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "18080:80"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- PMA_HOST=${DB_HOST:-db}
|
||||
- PMA_USER=${DB_ROOT_USER:-root}
|
||||
- PMA_PASSWORD=${DB_ROOT_PASSWORD}
|
||||
- UPLOAD_LIMIT=50M
|
||||
|
||||
# Redis 캐시 서버 (선택사항 - 세션 관리 및 속도 제한용)
|
||||
# redis:
|
||||
# image: redis:7-alpine
|
||||
# container_name: redis_hyungi_net
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
# volumes:
|
||||
# - redis_data:/data
|
||||
# command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-yourredispassword}
|
||||
|
||||
# Nginx 리버스 프록시 (선택사항 - HTTPS 및 로드밸런싱용)
|
||||
# nginx:
|
||||
# image: nginx:alpine
|
||||
# container_name: nginx_hyungi_net
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "80:80"
|
||||
# - "443:443"
|
||||
# volumes:
|
||||
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
# - ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
# depends_on:
|
||||
# - api
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
external: true
|
||||
name: 7a5a13668b77b18bc1efaf1811d09560aa3be0e722d782e8460cb74f37328d81 # 기존 볼륨명으로 연결
|
||||
# redis_data: # Redis 사용 시 주석 해제
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: hyungi_network
|
||||
@@ -1,79 +0,0 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const app = express();
|
||||
|
||||
// ✅ 요청 바디 용량 제한 확장
|
||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
// ✅ CORS 설정: 허용 origin 명시
|
||||
app.use(cors({
|
||||
origin: function (origin, callback) {
|
||||
const allowedOrigins = [
|
||||
'https://ahn.hyungi.net',
|
||||
'https://tech.hyungi.net',
|
||||
'https://pdf.hyungi.net'
|
||||
];
|
||||
if (!origin || allowedOrigins.includes(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('CORS 차단됨: ' + origin));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// ✅ 라우터 등록
|
||||
const authRoutes = require('./routes/authRoutes');
|
||||
const projectRoutes = require('./routes/projectRoutes');
|
||||
const workerRoutes = require('./routes/workerRoutes');
|
||||
const taskRoutes = require('./routes/taskRoutes');
|
||||
const processRoutes = require('./routes/processRoutes');
|
||||
const workReportRoutes = require('./routes/workReportRoutes');
|
||||
const cuttingPlanRoutes = require('./routes/cuttingPlanRoutes');
|
||||
const factoryInfoRoutes = require('./routes/factoryInfoRoutes');
|
||||
const equipmentListRoutes = require('./routes/equipmentListRoutes');
|
||||
const toolsRoute = require('./routes/toolsRoute');
|
||||
const uploadRoutes = require('./routes/uploadRoutes');
|
||||
const uploadBgRoutes = require('./routes/uploadBgRoutes');
|
||||
const dailyIssueReportRoutes = require('./routes/dailyIssueReportRoutes');
|
||||
const issueTypeRoutes = require('./routes/issueTypeRoutes');
|
||||
const healthRoutes = require('./routes/healthRoutes');
|
||||
const pipeSpecRoutes = require('./routes/pipeSpecRoutes');
|
||||
|
||||
// ahn.hyungi.net 배포용
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// ✅ 업로드된 파일 정적 라우팅 추가 (웹에서 이미지 접근 가능하게)
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// ✅ 각 라우트 등록
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/projects', projectRoutes);
|
||||
app.use('/api/workers', workerRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api/processes', processRoutes);
|
||||
app.use('/api/workreports', workReportRoutes);
|
||||
app.use('/api/cuttingplans', cuttingPlanRoutes);
|
||||
app.use('/api/factoryinfo', factoryInfoRoutes);
|
||||
app.use('/api/equipment', equipmentListRoutes);
|
||||
app.use('/api/tools', toolsRoute);
|
||||
app.use('/api/uploads', uploadRoutes);
|
||||
app.use('/api', uploadBgRoutes); // ✅ upload-bg 경로용
|
||||
app.use('/api/issue-reports', dailyIssueReportRoutes);
|
||||
app.use('/api/issue-types', issueTypeRoutes);
|
||||
app.use('/api', healthRoutes);
|
||||
app.use('/api/pipespecs', pipeSpecRoutes);
|
||||
|
||||
// ✅ 서버 실행
|
||||
const PORT = process.env.PORT || 3005;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 서버가 ${PORT}번 포트에서 실행 중...`);
|
||||
}).on('error', (err) => {
|
||||
console.error('❌ 서버 실행 중 오류 발생:', err);
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: '존재하지 않는 경로입니다.' });
|
||||
});
|
||||
Reference in New Issue
Block a user