Files
TK-FB-Project/api.hyungi.net/services/workReportService.js
Hyungi Ahn 74d3a78aa3 feat: 페이지 구조 재구성 및 사이드바 네비게이션 구현
- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성
  - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동
  - common/ → attendance/: 근태/휴가 관련 페이지 이동
  - admin/ 정리: safety-* 파일들을 safety/로 이동

- 사이드바 네비게이션 메뉴 구현
  - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리
  - 접기/펼치기 기능 및 상태 저장
  - 관리자 전용 메뉴 자동 표시/숨김

- 날씨 API 연동 (기상청 단기예보)
  - TBM 및 navbar에 현재 날씨 표시
  - weatherService.js 추가

- 안전 체크리스트 확장
  - 기본/날씨별/작업별 체크 유형 추가
  - checklist-manage.html 페이지 추가

- 이슈 신고 시스템 구현
  - workIssueController, workIssueModel, workIssueRoutes 추가

- DB 마이그레이션 파일 추가 (실행 대기)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 14:27:22 +09:00

451 lines
13 KiB
JavaScript

/**
* 작업 보고서 관리 서비스
*
* 작업 보고서 CRUD 및 조회 관련 비즈니스 로직 처리
*
* @author TK-FB-Project
* @since 2025-12-11
*/
const workReportModel = require('../models/workReportModel');
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
const logger = require('../utils/logger');
const { getDb } = require('../dbPool');
/**
* 작업 보고서 생성 (단일 또는 다중)
*/
const createWorkReportService = async (reportData) => {
const reports = Array.isArray(reportData) ? reportData : [reportData];
if (reports.length === 0) {
throw new ValidationError('보고서 데이터가 필요합니다');
}
logger.info('작업 보고서 생성 요청', { count: reports.length });
const workReport_ids = [];
try {
for (const report of reports) {
const id = await new Promise((resolve, reject) => {
workReportModel.create(report, (err, insertId) => {
if (err) reject(err);
else resolve(insertId);
});
});
workReport_ids.push(id);
}
logger.info('작업 보고서 생성 성공', {
count: workReport_ids.length,
ids: workReport_ids
});
return { workReport_ids };
} catch (error) {
logger.error('작업 보고서 생성 실패', {
count: reports.length,
error: error.message
});
throw new DatabaseError('작업 보고서 생성 중 오류가 발생했습니다');
}
};
/**
* 날짜별 작업 보고서 조회
*/
const getWorkReportsByDateService = async (date) => {
if (!date) {
throw new ValidationError('날짜가 필요합니다', {
required: ['date'],
received: { date }
});
}
logger.info('작업 보고서 날짜별 조회 요청', { date });
try {
const rows = await new Promise((resolve, reject) => {
workReportModel.getAllByDate(date, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
logger.info('작업 보고서 조회 성공', { date, count: rows.length });
return rows;
} catch (error) {
logger.error('작업 보고서 조회 실패', { date, error: error.message });
throw new DatabaseError('작업 보고서 조회 중 오류가 발생했습니다');
}
};
/**
* 기간별 작업 보고서 조회
*/
const getWorkReportsInRangeService = async (start, end) => {
if (!start || !end) {
throw new ValidationError('시작일과 종료일이 필요합니다', {
required: ['start', 'end'],
received: { start, end }
});
}
logger.info('작업 보고서 기간별 조회 요청', { start, end });
try {
const rows = await new Promise((resolve, reject) => {
workReportModel.getByRange(start, end, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
logger.info('작업 보고서 조회 성공', { start, end, count: rows.length });
return rows;
} catch (error) {
logger.error('작업 보고서 조회 실패', { start, end, error: error.message });
throw new DatabaseError('작업 보고서 조회 중 오류가 발생했습니다');
}
};
/**
* 단일 작업 보고서 조회
*/
const getWorkReportByIdService = async (id) => {
if (!id) {
throw new ValidationError('보고서 ID가 필요합니다');
}
logger.info('작업 보고서 조회 요청', { report_id: id });
try {
const row = await new Promise((resolve, reject) => {
workReportModel.getById(id, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
if (!row) {
logger.warn('작업 보고서를 찾을 수 없음', { report_id: id });
throw new NotFoundError('작업 보고서를 찾을 수 없습니다');
}
logger.info('작업 보고서 조회 성공', { report_id: id });
return row;
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
logger.error('작업 보고서 조회 실패', { report_id: id, error: error.message });
throw new DatabaseError('작업 보고서 조회 중 오류가 발생했습니다');
}
};
/**
* 작업 보고서 수정
*/
const updateWorkReportService = async (id, updateData) => {
if (!id) {
throw new ValidationError('보고서 ID가 필요합니다');
}
logger.info('작업 보고서 수정 요청', { report_id: id });
try {
const changes = await new Promise((resolve, reject) => {
workReportModel.update(id, updateData, (err, affectedRows) => {
if (err) reject(err);
else resolve(affectedRows);
});
});
if (changes === 0) {
logger.warn('작업 보고서를 찾을 수 없거나 변경사항 없음', { report_id: id });
throw new NotFoundError('작업 보고서를 찾을 수 없습니다');
}
logger.info('작업 보고서 수정 성공', { report_id: id, changes });
return { changes };
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
logger.error('작업 보고서 수정 실패', { report_id: id, error: error.message });
throw new DatabaseError('작업 보고서 수정 중 오류가 발생했습니다');
}
};
/**
* 작업 보고서 삭제
*/
const removeWorkReportService = async (id) => {
if (!id) {
throw new ValidationError('보고서 ID가 필요합니다');
}
logger.info('작업 보고서 삭제 요청', { report_id: id });
try {
const changes = await new Promise((resolve, reject) => {
workReportModel.remove(id, (err, affectedRows) => {
if (err) reject(err);
else resolve(affectedRows);
});
});
if (changes === 0) {
logger.warn('작업 보고서를 찾을 수 없음', { report_id: id });
throw new NotFoundError('작업 보고서를 찾을 수 없습니다');
}
logger.info('작업 보고서 삭제 성공', { report_id: id, changes });
return { changes };
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
logger.error('작업 보고서 삭제 실패', { report_id: id, error: error.message });
throw new DatabaseError('작업 보고서 삭제 중 오류가 발생했습니다');
}
};
/**
* 월간 요약 조회
*/
const getSummaryService = async (year, month) => {
if (!year || !month) {
throw new ValidationError('연도와 월이 필요합니다', {
required: ['year', 'month'],
received: { year, month }
});
}
const start = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-01`;
const end = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-31`;
logger.info('작업 보고서 월간 요약 조회 요청', { year, month, start, end });
try {
const rows = await new Promise((resolve, reject) => {
workReportModel.getByRange(start, end, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
if (!rows || rows.length === 0) {
logger.warn('월간 요약 데이터 없음', { year, month });
throw new NotFoundError('해당 기간의 작업 보고서가 없습니다');
}
logger.info('작업 보고서 월간 요약 조회 성공', {
year,
month,
count: rows.length
});
return rows;
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
logger.error('작업 보고서 월간 요약 조회 실패', {
year,
month,
error: error.message
});
throw new DatabaseError('월간 요약 조회 중 오류가 발생했습니다');
}
};
// ========== 부적합 원인 관리 서비스 ==========
/**
* 작업 보고서의 부적합 원인 목록 조회
*/
const getReportDefectsService = async (reportId) => {
const db = await getDb();
try {
const [rows] = await db.execute(`
SELECT
d.defect_id,
d.report_id,
d.error_type_id,
d.defect_hours,
d.note,
d.created_at,
et.name as error_type_name,
et.severity
FROM work_report_defects d
JOIN error_types et ON d.error_type_id = et.id
WHERE d.report_id = ?
ORDER BY d.created_at
`, [reportId]);
return rows;
} catch (error) {
logger.error('부적합 원인 조회 실패', { reportId, error: error.message });
throw new DatabaseError('부적합 원인 조회 중 오류가 발생했습니다');
}
};
/**
* 부적합 원인 저장 (전체 교체)
*/
const saveReportDefectsService = async (reportId, defects) => {
const db = await getDb();
try {
await db.query('START TRANSACTION');
// 기존 부적합 원인 삭제
await db.execute('DELETE FROM work_report_defects WHERE report_id = ?', [reportId]);
// 새 부적합 원인 추가
if (defects && defects.length > 0) {
for (const defect of defects) {
await db.execute(`
INSERT INTO work_report_defects (report_id, error_type_id, defect_hours, note)
VALUES (?, ?, ?, ?)
`, [reportId, defect.error_type_id, defect.defect_hours || 0, defect.note || null]);
}
}
// 총 부적합 시간 계산 및 daily_work_reports 업데이트
const totalErrorHours = defects
? defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0)
: 0;
await db.execute(`
UPDATE daily_work_reports
SET error_hours = ?,
error_type_id = ?,
work_status_id = ?
WHERE id = ?
`, [
totalErrorHours,
defects && defects.length > 0 ? defects[0].error_type_id : null,
totalErrorHours > 0 ? 2 : 1,
reportId
]);
await db.query('COMMIT');
logger.info('부적합 원인 저장 성공', { reportId, count: defects?.length || 0 });
return { success: true, count: defects?.length || 0, totalErrorHours };
} catch (error) {
await db.query('ROLLBACK');
logger.error('부적합 원인 저장 실패', { reportId, error: error.message });
throw new DatabaseError('부적합 원인 저장 중 오류가 발생했습니다');
}
};
/**
* 부적합 원인 추가 (단일)
*/
const addReportDefectService = async (reportId, defectData) => {
const db = await getDb();
try {
const [result] = await db.execute(`
INSERT INTO work_report_defects (report_id, error_type_id, defect_hours, note)
VALUES (?, ?, ?, ?)
`, [reportId, defectData.error_type_id, defectData.defect_hours || 0, defectData.note || null]);
// 총 부적합 시간 업데이트
await updateTotalErrorHours(reportId);
logger.info('부적합 원인 추가 성공', { reportId, defectId: result.insertId });
return { success: true, defect_id: result.insertId };
} catch (error) {
logger.error('부적합 원인 추가 실패', { reportId, error: error.message });
throw new DatabaseError('부적합 원인 추가 중 오류가 발생했습니다');
}
};
/**
* 부적합 원인 삭제
*/
const removeReportDefectService = async (defectId) => {
const db = await getDb();
try {
// report_id 먼저 조회
const [defect] = await db.execute('SELECT report_id FROM work_report_defects WHERE defect_id = ?', [defectId]);
if (defect.length === 0) {
throw new NotFoundError('부적합 원인을 찾을 수 없습니다');
}
const reportId = defect[0].report_id;
// 삭제
await db.execute('DELETE FROM work_report_defects WHERE defect_id = ?', [defectId]);
// 총 부적합 시간 업데이트
await updateTotalErrorHours(reportId);
logger.info('부적합 원인 삭제 성공', { defectId, reportId });
return { success: true };
} catch (error) {
if (error instanceof NotFoundError) throw error;
logger.error('부적합 원인 삭제 실패', { defectId, error: error.message });
throw new DatabaseError('부적합 원인 삭제 중 오류가 발생했습니다');
}
};
/**
* 총 부적합 시간 업데이트 헬퍼
*/
const updateTotalErrorHours = async (reportId) => {
const db = await getDb();
const [result] = await db.execute(`
SELECT COALESCE(SUM(defect_hours), 0) as total
FROM work_report_defects
WHERE report_id = ?
`, [reportId]);
const totalErrorHours = result[0].total || 0;
// 첫 번째 부적합 원인의 error_type_id를 대표값으로 사용
const [firstDefect] = await db.execute(`
SELECT error_type_id FROM work_report_defects WHERE report_id = ? ORDER BY created_at LIMIT 1
`, [reportId]);
await db.execute(`
UPDATE daily_work_reports
SET error_hours = ?,
error_type_id = ?,
work_status_id = ?
WHERE id = ?
`, [
totalErrorHours,
firstDefect.length > 0 ? firstDefect[0].error_type_id : null,
totalErrorHours > 0 ? 2 : 1,
reportId
]);
};
module.exports = {
createWorkReportService,
getWorkReportsByDateService,
getWorkReportsInRangeService,
getWorkReportByIdService,
updateWorkReportService,
removeWorkReportService,
getSummaryService,
// 부적합 원인 관리
getReportDefectsService,
saveReportDefectsService,
addReportDefectService,
removeReportDefectService
};