feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
519
deploy/tkfb-package/api.hyungi.net/models/WorkAnalysis.js
Normal file
519
deploy/tkfb-package/api.hyungi.net/models/WorkAnalysis.js
Normal file
@@ -0,0 +1,519 @@
|
||||
// models/WorkAnalysis.js - 향상된 버전
|
||||
|
||||
class WorkAnalysis {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
// 기본 통계 조회
|
||||
async getBasicStats(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
COALESCE(SUM(work_hours), 0) as total_hours,
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(DISTINCT project_id) as active_projects,
|
||||
COUNT(DISTINCT worker_id) as active_workers,
|
||||
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_reports,
|
||||
ROUND(AVG(work_hours), 2) as avg_hours_per_report
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
const stats = results[0];
|
||||
|
||||
const errorRate = stats.total_reports > 0
|
||||
? (stats.error_reports / stats.total_reports) * 100
|
||||
: 0;
|
||||
|
||||
return {
|
||||
totalHours: parseFloat(stats.total_hours) || 0,
|
||||
totalReports: parseInt(stats.total_reports) || 0,
|
||||
activeProjects: parseInt(stats.active_projects) || 0,
|
||||
activeworkers: parseInt(stats.active_workers) || 0,
|
||||
errorRate: parseFloat(errorRate.toFixed(2)) || 0,
|
||||
avgHoursPerReport: parseFloat(stats.avg_hours_per_report) || 0
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`기본 통계 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 일별 작업시간 추이
|
||||
async getDailyTrend(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
report_date as date,
|
||||
SUM(work_hours) as hours,
|
||||
COUNT(*) as reports,
|
||||
COUNT(DISTINCT worker_id) as workers,
|
||||
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as errors
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
GROUP BY report_date
|
||||
ORDER BY report_date
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
date: row.date,
|
||||
hours: parseFloat(row.hours) || 0,
|
||||
reports: parseInt(row.reports) || 0,
|
||||
workers: parseInt(row.workers) || 0,
|
||||
errors: parseInt(row.errors) || 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`일별 추이 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 통계
|
||||
async getWorkerStats(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
SUM(dwr.work_hours) as totalHours,
|
||||
COUNT(*) as totalReports,
|
||||
ROUND(AVG(dwr.work_hours), 2) as avgHours,
|
||||
COUNT(DISTINCT dwr.project_id) as projectCount,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
|
||||
COUNT(DISTINCT dwr.report_date) as workingDays
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.worker_id, w.worker_name
|
||||
ORDER BY totalHours DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
worker_id: row.worker_id,
|
||||
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
|
||||
totalHours: parseFloat(row.totalHours) || 0,
|
||||
totalReports: parseInt(row.totalReports) || 0,
|
||||
avgHours: parseFloat(row.avgHours) || 0,
|
||||
projectCount: parseInt(row.projectCount) || 0,
|
||||
errorCount: parseInt(row.errorCount) || 0,
|
||||
workingDays: parseInt(row.workingDays) || 0,
|
||||
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`작업자별 통계 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트별 통계
|
||||
async getProjectStats(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.project_id,
|
||||
p.project_name,
|
||||
SUM(dwr.work_hours) as totalHours,
|
||||
COUNT(*) as totalReports,
|
||||
COUNT(DISTINCT dwr.worker_id) as workerCount,
|
||||
ROUND(AVG(dwr.work_hours), 2) as avgHours,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
|
||||
COUNT(DISTINCT dwr.report_date) as activeDays
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.project_id, p.project_name
|
||||
ORDER BY totalHours DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name || `프로젝트 ${row.project_id}`,
|
||||
totalHours: parseFloat(row.totalHours) || 0,
|
||||
totalReports: parseInt(row.totalReports) || 0,
|
||||
workerCount: parseInt(row.workerCount) || 0,
|
||||
avgHours: parseFloat(row.avgHours) || 0,
|
||||
errorCount: parseInt(row.errorCount) || 0,
|
||||
activeDays: parseInt(row.activeDays) || 0,
|
||||
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`프로젝트별 통계 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업유형별 통계
|
||||
async getWorkTypeStats(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.work_type_id,
|
||||
wt.name as work_type_name,
|
||||
SUM(dwr.work_hours) as totalHours,
|
||||
COUNT(*) as totalReports,
|
||||
ROUND(AVG(dwr.work_hours), 2) as avgHours,
|
||||
COUNT(DISTINCT dwr.worker_id) as workerCount,
|
||||
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
|
||||
COUNT(DISTINCT dwr.project_id) as projectCount
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.work_type_id, wt.name
|
||||
ORDER BY totalHours DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
|
||||
totalHours: parseFloat(row.totalHours) || 0,
|
||||
totalReports: parseInt(row.totalReports) || 0,
|
||||
avgHours: parseFloat(row.avgHours) || 0,
|
||||
workerCount: parseInt(row.workerCount) || 0,
|
||||
errorCount: parseInt(row.errorCount) || 0,
|
||||
projectCount: parseInt(row.projectCount) || 0,
|
||||
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`작업유형별 통계 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 최근 작업 현황
|
||||
async getRecentWork(startDate, endDate, limit = 50) {
|
||||
// work_type_id 컬럼에는 task_id가 저장됨 (tasks 테이블 우선 조회)
|
||||
// task_id로 매칭되면 해당 task의 work_type_id로 공정(대분류) 조회
|
||||
// 매칭 안 되면 직접 work_types 테이블 조회 (레거시 데이터 호환)
|
||||
// error_type_id는 issue_report_items 테이블 참조 (issue-categories.html에서 관리)
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.id,
|
||||
dwr.report_date,
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
dwr.project_id,
|
||||
p.project_name,
|
||||
p.job_no,
|
||||
dwr.work_type_id as original_work_type_id,
|
||||
COALESCE(
|
||||
CASE WHEN t.task_id IS NOT NULL THEN t.work_type_id ELSE NULL END,
|
||||
wt.id
|
||||
) as work_type_id,
|
||||
COALESCE(
|
||||
CASE WHEN t.task_id IS NOT NULL THEN wt2.name ELSE NULL END,
|
||||
wt.name
|
||||
) as work_type_name,
|
||||
t.task_name as task_name,
|
||||
dwr.work_status_id,
|
||||
wst.name as work_status_name,
|
||||
dwr.error_type_id,
|
||||
iri.item_name as error_type_name,
|
||||
irc.category_name as error_category_name,
|
||||
dwr.work_hours,
|
||||
dwr.created_by,
|
||||
u.name as created_by_name,
|
||||
dwr.created_at
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id
|
||||
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
|
||||
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
|
||||
LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
LEFT JOIN users u ON dwr.created_by = u.user_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
ORDER BY dwr.created_at DESC
|
||||
LIMIT ?
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate, parseInt(limit)]);
|
||||
return results.map(row => ({
|
||||
id: row.id,
|
||||
report_date: row.report_date,
|
||||
worker_id: row.worker_id,
|
||||
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name || `프로젝트 ${row.project_id}`,
|
||||
job_no: row.job_no || 'N/A',
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name || `작업유형 ${row.original_work_type_id}`,
|
||||
task_name: row.task_name || null,
|
||||
work_status_id: row.work_status_id,
|
||||
work_status_name: row.work_status_name || '정상',
|
||||
error_type_id: row.error_type_id,
|
||||
error_type_name: row.error_type_name || null,
|
||||
error_category_name: row.error_category_name || null,
|
||||
work_hours: parseFloat(row.work_hours) || 0,
|
||||
created_by: row.created_by,
|
||||
created_by_name: row.created_by_name || '미지정',
|
||||
created_at: row.created_at
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`최근 작업 현황 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 요일별 패턴 분석
|
||||
async getWeekdayPattern(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
DAYOFWEEK(report_date) as day_of_week,
|
||||
CASE DAYOFWEEK(report_date)
|
||||
WHEN 1 THEN '일요일'
|
||||
WHEN 2 THEN '월요일'
|
||||
WHEN 3 THEN '화요일'
|
||||
WHEN 4 THEN '수요일'
|
||||
WHEN 5 THEN '목요일'
|
||||
WHEN 6 THEN '금요일'
|
||||
WHEN 7 THEN '토요일'
|
||||
END as day_name,
|
||||
SUM(work_hours) as total_hours,
|
||||
COUNT(*) as total_reports,
|
||||
ROUND(AVG(work_hours), 2) as avg_hours,
|
||||
COUNT(DISTINCT worker_id) as active_workers
|
||||
FROM daily_work_reports
|
||||
WHERE report_date BETWEEN ? AND ?
|
||||
GROUP BY DAYOFWEEK(report_date)
|
||||
ORDER BY day_of_week
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
dayOfWeek: row.day_of_week,
|
||||
dayName: row.day_name,
|
||||
totalHours: parseFloat(row.total_hours) || 0,
|
||||
totalReports: parseInt(row.total_reports) || 0,
|
||||
avgHours: parseFloat(row.avg_hours) || 0,
|
||||
activeworkers: parseInt(row.active_workers) || 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`요일별 패턴 분석 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 분석
|
||||
// error_type_id는 issue_report_items 테이블 참조 (issue-categories.html에서 관리)
|
||||
async getErrorAnalysis(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.error_type_id,
|
||||
iri.item_name as error_type_name,
|
||||
irc.category_name as error_category_name,
|
||||
COUNT(*) as error_count,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(DISTINCT dwr.worker_id) as affected_workers,
|
||||
COUNT(DISTINCT dwr.project_id) as affected_projects
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id
|
||||
LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
AND dwr.work_status_id = 2
|
||||
GROUP BY dwr.error_type_id, iri.item_name, irc.category_name
|
||||
ORDER BY error_count DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
error_type_id: row.error_type_id,
|
||||
error_type_name: row.error_type_name || `에러유형 ${row.error_type_id}`,
|
||||
error_category_name: row.error_category_name || null,
|
||||
errorCount: parseInt(row.error_count) || 0,
|
||||
totalHours: parseFloat(row.total_hours) || 0,
|
||||
affectedworkers: parseInt(row.affected_workers) || 0,
|
||||
affectedProjects: parseInt(row.affected_projects) || 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`에러 분석 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 비교 분석
|
||||
async getMonthlyComparison(year) {
|
||||
const query = `
|
||||
SELECT
|
||||
MONTH(report_date) as month,
|
||||
MONTHNAME(report_date) as month_name,
|
||||
SUM(work_hours) as total_hours,
|
||||
COUNT(*) as total_reports,
|
||||
COUNT(DISTINCT worker_id) as active_workers,
|
||||
COUNT(DISTINCT project_id) as active_projects,
|
||||
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_count
|
||||
FROM daily_work_reports
|
||||
WHERE YEAR(report_date) = ?
|
||||
GROUP BY MONTH(report_date), MONTHNAME(report_date)
|
||||
ORDER BY month
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [year]);
|
||||
return results.map(row => ({
|
||||
month: row.month,
|
||||
monthName: row.month_name,
|
||||
totalHours: parseFloat(row.total_hours) || 0,
|
||||
totalReports: parseInt(row.total_reports) || 0,
|
||||
activeworkers: parseInt(row.active_workers) || 0,
|
||||
activeProjects: parseInt(row.active_projects) || 0,
|
||||
errorCount: parseInt(row.error_count) || 0,
|
||||
errorRate: row.total_reports > 0 ? parseFloat(((row.error_count / row.total_reports) * 100).toFixed(2)) : 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`월별 비교 분석 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 전문분야 분석
|
||||
async getWorkerSpecialization(startDate, endDate) {
|
||||
const query = `
|
||||
SELECT
|
||||
dwr.worker_id,
|
||||
w.worker_name,
|
||||
dwr.work_type_id,
|
||||
wt.name as work_type_name,
|
||||
dwr.project_id,
|
||||
p.project_name,
|
||||
SUM(dwr.work_hours) as totalHours,
|
||||
COUNT(*) as totalReports,
|
||||
ROUND((SUM(dwr.work_hours) / (
|
||||
SELECT SUM(work_hours)
|
||||
FROM daily_work_reports
|
||||
WHERE worker_id = dwr.worker_id
|
||||
AND report_date BETWEEN ? AND ?
|
||||
)) * 100, 2) as percentage
|
||||
FROM daily_work_reports dwr
|
||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN projects p ON dwr.project_id = p.project_id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.worker_id, w.worker_name, dwr.work_type_id, wt.name, dwr.project_id, p.project_name
|
||||
HAVING totalHours > 0
|
||||
ORDER BY dwr.worker_id, totalHours DESC
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate, startDate, endDate]);
|
||||
return results.map(row => ({
|
||||
worker_id: row.worker_id,
|
||||
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
|
||||
work_type_id: row.work_type_id,
|
||||
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
|
||||
project_id: row.project_id,
|
||||
project_name: row.project_name || `프로젝트 ${row.project_id}`,
|
||||
totalHours: parseFloat(row.totalHours) || 0,
|
||||
totalReports: parseInt(row.totalReports) || 0,
|
||||
percentage: parseFloat(row.percentage) || 0
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(`작업자별 전문분야 분석 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 대시보드용 종합 데이터
|
||||
async getDashboardData(startDate, endDate) {
|
||||
try {
|
||||
// 병렬로 모든 데이터 조회
|
||||
const [
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork
|
||||
] = await Promise.all([
|
||||
this.getBasicStats(startDate, endDate),
|
||||
this.getDailyTrend(startDate, endDate),
|
||||
this.getWorkerStats(startDate, endDate),
|
||||
this.getProjectStats(startDate, endDate),
|
||||
this.getWorkTypeStats(startDate, endDate),
|
||||
this.getRecentWork(startDate, endDate, 20)
|
||||
]);
|
||||
|
||||
return {
|
||||
stats,
|
||||
dailyTrend,
|
||||
workerStats,
|
||||
projectStats,
|
||||
workTypeStats,
|
||||
recentWork,
|
||||
metadata: {
|
||||
period: `${startDate} ~ ${endDate}`,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`대시보드 데이터 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
// 프로젝트별-작업별 시간 분석용 데이터 조회 (공정/대분류 기준)
|
||||
async getProjectWorkTypeRawData(startDate, endDate) {
|
||||
// work_type_id 컬럼에는 task_id가 저장됨 (tasks 테이블 우선 조회)
|
||||
// task_id로 매칭되면 해당 task의 work_type_id로 공정 조회
|
||||
// 매칭 안 되면 직접 work_types 조회 (레거시 데이터 호환)
|
||||
const query = `
|
||||
SELECT
|
||||
COALESCE(p.project_id, dwr.project_id) as project_id,
|
||||
COALESCE(p.project_name, CONCAT('프로젝트 ', dwr.project_id)) as project_name,
|
||||
COALESCE(p.job_no, 'N/A') as job_no,
|
||||
COALESCE(
|
||||
CASE WHEN t.task_id IS NOT NULL THEN t.work_type_id ELSE NULL END,
|
||||
wt.id
|
||||
) as work_type_id,
|
||||
COALESCE(
|
||||
CASE WHEN t.task_id IS NOT NULL THEN wt2.name ELSE NULL END,
|
||||
wt.name,
|
||||
CONCAT('작업유형 ', 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
|
||||
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON dwr.work_type_id = t.task_id
|
||||
LEFT JOIN work_types wt2 ON t.work_type_id = wt2.id
|
||||
WHERE dwr.report_date BETWEEN ? AND ?
|
||||
GROUP BY dwr.project_id, p.project_name, p.job_no,
|
||||
COALESCE(
|
||||
CASE WHEN t.task_id IS NOT NULL THEN t.work_type_id ELSE NULL END,
|
||||
wt.id
|
||||
),
|
||||
COALESCE(
|
||||
CASE WHEN t.task_id IS NOT NULL THEN wt2.name ELSE NULL END,
|
||||
wt.name
|
||||
)
|
||||
ORDER BY p.project_name, work_type_name
|
||||
`;
|
||||
|
||||
try {
|
||||
const [results] = await this.db.execute(query, [startDate, endDate]);
|
||||
return results;
|
||||
} catch (error) {
|
||||
throw new Error(`프로젝트-작업유형별 원본 데이터 조회 실패: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WorkAnalysis;
|
||||
115
deploy/tkfb-package/api.hyungi.net/models/analysisModel.js
Normal file
115
deploy/tkfb-package/api.hyungi.net/models/analysisModel.js
Normal file
@@ -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<object>} - 요약, 프로젝트별, 작업자별, 작업별 집계 데이터 및 상세 내역
|
||||
*/
|
||||
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
|
||||
};
|
||||
530
deploy/tkfb-package/api.hyungi.net/models/attendanceModel.js
Normal file
530
deploy/tkfb-package/api.hyungi.net/models/attendanceModel.js
Normal file
@@ -0,0 +1,530 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
class AttendanceModel {
|
||||
// 일일 근태 기록 조회
|
||||
static async getDailyAttendanceRecords(date, workerId = null) {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
dar.*,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
wat.type_name as attendance_type_name,
|
||||
wat.type_code as attendance_type_code,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code as vacation_type_code,
|
||||
vt.deduct_days as vacation_days
|
||||
FROM daily_attendance_records dar
|
||||
LEFT JOIN workers w ON dar.worker_id = w.worker_id
|
||||
LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id
|
||||
LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id
|
||||
WHERE dar.record_date = ?
|
||||
`;
|
||||
|
||||
const params = [date];
|
||||
|
||||
if (workerId) {
|
||||
query += ' AND dar.worker_id = ?';
|
||||
params.push(workerId);
|
||||
}
|
||||
|
||||
query += ' ORDER BY w.worker_name';
|
||||
|
||||
const [rows] = await db.execute(query, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 기간별 근태 기록 조회 (월별 조회용)
|
||||
static async getDailyRecords(startDate, endDate, workerId = null) {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
dar.*,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
wat.type_name as attendance_type_name,
|
||||
wat.type_code as attendance_type_code,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code as vacation_type_code,
|
||||
vt.deduct_days as vacation_days
|
||||
FROM daily_attendance_records dar
|
||||
LEFT JOIN workers w ON dar.worker_id = w.worker_id
|
||||
LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id
|
||||
LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id
|
||||
WHERE dar.record_date BETWEEN ? AND ?
|
||||
`;
|
||||
|
||||
const params = [startDate, endDate];
|
||||
|
||||
if (workerId) {
|
||||
query += ' AND dar.worker_id = ?';
|
||||
params.push(workerId);
|
||||
}
|
||||
|
||||
query += ' ORDER BY dar.record_date ASC';
|
||||
|
||||
const [rows] = await db.execute(query, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 작업 보고서와 근태 기록 동기화 (시간 합산 및 상태 업데이트)
|
||||
static async syncWithWorkReports(workerId, date) {
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 해당 날짜의 총 작업 시간 계산
|
||||
const [reportStats] = await db.execute(`
|
||||
SELECT
|
||||
COALESCE(SUM(work_hours), 0) as total_hours,
|
||||
COUNT(*) as report_count
|
||||
FROM daily_work_reports
|
||||
WHERE worker_id = ? AND report_date = ?
|
||||
`, [workerId, date]);
|
||||
|
||||
const totalHours = parseFloat(reportStats[0].total_hours || 0);
|
||||
const reportCount = reportStats[0].report_count;
|
||||
|
||||
// 2. 근태 유형 및 상태 결정
|
||||
// 기본 규칙: 0시간 -> incomplete, <8시간 -> partial, 8시간 -> complete, >8시간 -> overtime
|
||||
// (휴가는 별도 로직이지만 여기서 덮어쓰지 않도록 주의해야 함. 하지만 작업보고서가 추가되면 실 근무로 간주)
|
||||
|
||||
let status = 'incomplete';
|
||||
let typeCode = 'REGULAR'; // 기본값
|
||||
|
||||
if (totalHours === 0) {
|
||||
status = 'incomplete';
|
||||
} else if (totalHours < 8) {
|
||||
status = 'partial';
|
||||
typeCode = 'PARTIAL';
|
||||
} else if (totalHours === 8) {
|
||||
status = 'complete';
|
||||
typeCode = 'REGULAR';
|
||||
} else {
|
||||
status = 'overtime';
|
||||
typeCode = 'OVERTIME';
|
||||
}
|
||||
|
||||
// 근태 유형 ID 조회
|
||||
const [types] = await db.execute('SELECT id FROM work_attendance_types WHERE type_code = ?', [typeCode]);
|
||||
const typeId = types[0]?.id;
|
||||
|
||||
// 3. 기록 업데이트 (휴가 정보는 유지)
|
||||
// 기존 기록 조회
|
||||
const [existing] = await db.execute(
|
||||
'SELECT id, vacation_type_id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?',
|
||||
[workerId, date]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// 휴가가 설정되어 있고 시간이 0이면 휴가 상태 유지, 시간이 있으면 근무+휴가 복합 상태일 수 있음
|
||||
// 여기서는 단순화하여 근무 시간이 있으면 근무 상태로 업데이트 (단, vacation_type_id는 유지)
|
||||
|
||||
const recordId = existing[0].id;
|
||||
// 만약 기존 상태가 'vacation'이고 근무시간이 0이면 업데이트 건너뛸 수도 있지만,
|
||||
// 작업보고서가 삭제되어 0이 된 경우도 있으므로 업데이트는 수행해야 함.
|
||||
|
||||
await db.execute(`
|
||||
UPDATE daily_attendance_records
|
||||
SET
|
||||
total_work_hours = ?,
|
||||
attendance_type_id = ?,
|
||||
status = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [totalHours, typeId, status, recordId]);
|
||||
|
||||
return { synced: true, totalHours, status };
|
||||
} else {
|
||||
// 기록이 없으면 생성 (일반적으로는 initializeDailyRecords로 생성되어 있어야 함)
|
||||
// 생성자가 명확하지 않으므로 시스템(1) 또는 알 수 없음 처리
|
||||
await db.execute(`
|
||||
INSERT INTO daily_attendance_records
|
||||
(record_date, worker_id, total_work_hours, attendance_type_id, status, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, 1)
|
||||
`, [date, workerId, totalHours, typeId, status]);
|
||||
|
||||
return { synced: true, totalHours, status, created: true };
|
||||
}
|
||||
}
|
||||
|
||||
// 일일 근태 기록 초기화 (모든 활성 작업자에 대한 기본 레코드 생성)
|
||||
static async initializeDailyRecords(date, createdBy) {
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 활성 작업자 조회
|
||||
const [workers] = await db.execute(
|
||||
'SELECT worker_id FROM workers WHERE status = "active"' // is_active check not needed as status covers it based on previous fix? Wait, previous fix used status='active'.
|
||||
);
|
||||
|
||||
if (workers.length === 0) return { inserted: 0 };
|
||||
|
||||
// 2. 일일 근태 레코드 일괄 생성 (이미 존재하면 무시)
|
||||
// VALUES (...), (...), ...
|
||||
const values = workers.map(w => [date, w.worker_id, 'incomplete', createdBy]);
|
||||
|
||||
// Bulk INSERT IGNORE
|
||||
// Note: mysql2 execute doesn't support nested arrays for bulk insert easily with placeholder ?
|
||||
// We should build the query or use query method for pool?
|
||||
// Using simple loop for safety and compatibility or building string.
|
||||
|
||||
let insertedCount = 0;
|
||||
|
||||
// 트랜잭션 사용 권장
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
for (const w of workers) {
|
||||
const [result] = await conn.execute(`
|
||||
INSERT IGNORE INTO daily_attendance_records
|
||||
(record_date, worker_id, status, created_by)
|
||||
VALUES (?, ?, 'incomplete', ?)
|
||||
`, [date, w.worker_id, createdBy]);
|
||||
|
||||
insertedCount += result.affectedRows;
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
|
||||
return { inserted: insertedCount, total_active_workers: workers.length };
|
||||
}
|
||||
|
||||
// 근태 기록 생성 또는 업데이트
|
||||
static async upsertAttendanceRecord(recordData) {
|
||||
const db = await getDb();
|
||||
|
||||
const {
|
||||
record_date,
|
||||
worker_id,
|
||||
total_work_hours = 8,
|
||||
work_attendance_type_id = 1,
|
||||
vacation_type_id = null,
|
||||
is_overtime_approved = false,
|
||||
created_by = 1
|
||||
} = recordData;
|
||||
|
||||
const attendance_type_id = work_attendance_type_id;
|
||||
|
||||
// 기존 기록 확인
|
||||
const [existing] = await db.execute(
|
||||
'SELECT id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?',
|
||||
[worker_id, record_date]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// 업데이트
|
||||
const [result] = await db.execute(`
|
||||
UPDATE daily_attendance_records
|
||||
SET
|
||||
total_work_hours = ?,
|
||||
attendance_type_id = ?,
|
||||
vacation_type_id = ?,
|
||||
is_overtime_approved = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`, [
|
||||
total_work_hours,
|
||||
attendance_type_id,
|
||||
vacation_type_id,
|
||||
is_overtime_approved,
|
||||
existing[0].id
|
||||
]);
|
||||
|
||||
return { id: existing[0].id, affected: result.affectedRows };
|
||||
} else {
|
||||
// 생성
|
||||
const [result] = await db.execute(`
|
||||
INSERT INTO daily_attendance_records (
|
||||
record_date, worker_id, total_work_hours, attendance_type_id,
|
||||
vacation_type_id, is_overtime_approved, created_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
record_date,
|
||||
worker_id,
|
||||
total_work_hours,
|
||||
attendance_type_id,
|
||||
vacation_type_id,
|
||||
is_overtime_approved,
|
||||
created_by
|
||||
]);
|
||||
|
||||
return { id: result.insertId, affected: result.affectedRows };
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자별 근태 현황 조회 (대시보드용)
|
||||
static async getWorkerAttendanceStatus(date) {
|
||||
const db = await getDb();
|
||||
|
||||
// 모든 작업자와 해당 날짜의 근태 기록을 조회
|
||||
const [rows] = await db.execute(`
|
||||
SELECT
|
||||
w.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
COALESCE(dar.total_work_hours, 0) as total_work_hours,
|
||||
COALESCE(dar.status, 'incomplete') as status,
|
||||
dar.is_vacation_processed,
|
||||
dar.overtime_approved,
|
||||
wat.type_name as attendance_type_name,
|
||||
wat.type_code as attendance_type_code,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code as vacation_type_code,
|
||||
dar.notes,
|
||||
-- 작업 건수 계산
|
||||
(SELECT COUNT(*) FROM daily_work_reports dwr
|
||||
WHERE dwr.worker_id = w.worker_id AND dwr.report_date = ?) as work_count,
|
||||
-- 오류 건수 계산
|
||||
(SELECT COUNT(*) FROM daily_work_reports dwr
|
||||
WHERE dwr.worker_id = w.worker_id AND dwr.report_date = ? AND dwr.work_status_id = 2) as error_count
|
||||
FROM workers w
|
||||
LEFT JOIN daily_attendance_records dar ON w.worker_id = dar.worker_id AND dar.record_date = ?
|
||||
LEFT JOIN work_attendance_types wat ON dar.attendance_type_id = wat.id
|
||||
LEFT JOIN vacation_types vt ON dar.vacation_type_id = vt.id
|
||||
WHERE w.is_active = TRUE
|
||||
ORDER BY w.worker_name
|
||||
`, [date, date, date]);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 휴가 처리
|
||||
static async processVacation(workerId, date, vacationType, createdBy) {
|
||||
const db = await getDb();
|
||||
|
||||
// 휴가 유형 정보 조회
|
||||
const [vacationTypes] = await db.execute(
|
||||
'SELECT id, type_code, type_name, deduct_days, is_active, created_at, updated_at FROM vacation_types WHERE type_code = ?',
|
||||
[vacationType]
|
||||
);
|
||||
|
||||
if (vacationTypes.length === 0) {
|
||||
throw new Error('유효하지 않은 휴가 유형입니다.');
|
||||
}
|
||||
|
||||
const vacationTypeInfo = vacationTypes[0];
|
||||
|
||||
// 현재 작업 시간 조회
|
||||
const [workHours] = await db.execute(`
|
||||
SELECT COALESCE(SUM(work_hours), 0) as total_hours
|
||||
FROM daily_work_reports
|
||||
WHERE worker_id = ? AND report_date = ?
|
||||
`, [workerId, date]);
|
||||
|
||||
const currentHours = parseFloat(workHours[0].total_hours);
|
||||
// deduct_days를 시간으로 변환 (1일 = 8시간)
|
||||
const vacationHours = parseFloat(vacationTypeInfo.deduct_days) * 8;
|
||||
const totalHours = currentHours + vacationHours;
|
||||
|
||||
// 근로 유형 결정
|
||||
let attendanceTypeCode = 'VACATION';
|
||||
let status = 'vacation';
|
||||
|
||||
if (totalHours >= 8) {
|
||||
attendanceTypeCode = totalHours > 8 ? 'OVERTIME' : 'REGULAR';
|
||||
status = totalHours > 8 ? 'overtime' : 'complete';
|
||||
}
|
||||
|
||||
const [attendanceTypes] = await db.execute(
|
||||
'SELECT id FROM work_attendance_types WHERE type_code = ?',
|
||||
[attendanceTypeCode]
|
||||
);
|
||||
|
||||
// 휴가 작업 기록 생성 (프로젝트 ID 13 = "연차/휴무", work_type_id 1 = 기본)
|
||||
await db.execute(`
|
||||
INSERT INTO daily_work_reports (
|
||||
report_date, worker_id, project_id, work_type_id, work_status_id,
|
||||
work_hours, description, created_by
|
||||
) VALUES (?, ?, 13, 1, 1, ?, ?, ?)
|
||||
`, [
|
||||
date,
|
||||
workerId,
|
||||
vacationHours,
|
||||
`${vacationTypeInfo.type_name} 처리`,
|
||||
createdBy
|
||||
]);
|
||||
|
||||
// 근태 기록 업데이트
|
||||
const attendanceData = {
|
||||
record_date: date,
|
||||
worker_id: workerId,
|
||||
total_work_hours: totalHours,
|
||||
work_attendance_type_id: attendanceTypes[0]?.id,
|
||||
vacation_type_id: vacationTypeInfo.id,
|
||||
is_overtime_approved: false,
|
||||
created_by: createdBy
|
||||
};
|
||||
|
||||
return await this.upsertAttendanceRecord(attendanceData);
|
||||
}
|
||||
|
||||
// 초과근무 승인
|
||||
static async approveOvertime(workerId, date, approvedBy) {
|
||||
const db = await getDb();
|
||||
|
||||
const [result] = await db.execute(`
|
||||
UPDATE daily_attendance_records
|
||||
SET
|
||||
overtime_approved = TRUE,
|
||||
overtime_approved_by = ?,
|
||||
overtime_approved_at = CURRENT_TIMESTAMP,
|
||||
updated_by = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE worker_id = ? AND record_date = ?
|
||||
`, [approvedBy, approvedBy, workerId, date]);
|
||||
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
// 근로 유형 목록 조회
|
||||
static async getAttendanceTypes() {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.execute(
|
||||
'SELECT id, type_code, type_name, description, is_active, created_at, updated_at FROM work_attendance_types WHERE is_active = TRUE ORDER BY id'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 휴가 유형 목록 조회
|
||||
static async getVacationTypes() {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.execute(
|
||||
'SELECT id, type_code, type_name, deduct_days, is_active, created_at, updated_at FROM vacation_types WHERE is_active = TRUE ORDER BY deduct_days DESC'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 작업자 휴가 잔여 조회
|
||||
static async getWorkerVacationBalance(workerId, year = null) {
|
||||
const db = await getDb();
|
||||
const currentYear = year || new Date().getFullYear();
|
||||
|
||||
const [rows] = await db.execute(`
|
||||
SELECT id, worker_id, year, total_annual_leave, used_annual_leave, notes, created_at, updated_at FROM worker_vacation_balance
|
||||
WHERE worker_id = ? AND year = ?
|
||||
`, [workerId, currentYear]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
// 기본 연차 생성 (15일)
|
||||
await db.execute(`
|
||||
INSERT INTO worker_vacation_balance (worker_id, year, total_annual_leave)
|
||||
VALUES (?, ?, 15.0)
|
||||
`, [workerId, currentYear]);
|
||||
|
||||
return {
|
||||
worker_id: workerId,
|
||||
year: currentYear,
|
||||
total_annual_leave: 15.0,
|
||||
used_annual_leave: 0,
|
||||
remaining_annual_leave: 15.0
|
||||
};
|
||||
}
|
||||
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
// 월별 근태 통계
|
||||
static async getMonthlyAttendanceStats(year, month, workerId = null) {
|
||||
const db = await getDb();
|
||||
|
||||
// work_attendance_types: 1=NORMAL, 2=LATE, 3=EARLY_LEAVE, 4=ABSENT, 5=VACATION
|
||||
// vacation_types: 1=ANNUAL(연차), 2=HALF_ANNUAL(반차), 3=SICK(병가), 4=SPECIAL(경조사)
|
||||
let query = `
|
||||
SELECT
|
||||
w.worker_id,
|
||||
w.worker_name,
|
||||
COUNT(CASE WHEN dar.attendance_type_id = 1 AND (dar.is_overtime_approved = 0 OR dar.is_overtime_approved IS NULL) THEN 1 END) as regular_days,
|
||||
COUNT(CASE WHEN dar.is_overtime_approved = 1 OR dar.total_work_hours > 8 THEN 1 END) as overtime_days,
|
||||
COUNT(CASE WHEN dar.attendance_type_id = 5 AND dar.vacation_type_id = 1 THEN 1 END) as vacation_days,
|
||||
COUNT(CASE WHEN dar.vacation_type_id = 2 THEN 1 END) as partial_days,
|
||||
COUNT(CASE WHEN dar.attendance_type_id = 4 THEN 1 END) as incomplete_days,
|
||||
COALESCE(SUM(dar.total_work_hours), 0) as total_work_hours,
|
||||
COALESCE(AVG(dar.total_work_hours), 0) as avg_work_hours
|
||||
FROM workers w
|
||||
LEFT JOIN daily_attendance_records dar ON w.worker_id = dar.worker_id
|
||||
AND YEAR(dar.record_date) = ? AND MONTH(dar.record_date) = ?
|
||||
WHERE w.employment_status = 'employed'
|
||||
`;
|
||||
|
||||
const params = [year, month];
|
||||
|
||||
if (workerId) {
|
||||
query += ' AND w.worker_id = ?';
|
||||
params.push(workerId);
|
||||
}
|
||||
|
||||
query += ' GROUP BY w.worker_id, w.worker_name ORDER BY w.worker_name';
|
||||
|
||||
const [rows] = await db.execute(query, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 출근 체크 기록 생성 또는 업데이트
|
||||
static async upsertCheckin(checkinData) {
|
||||
const db = await getDb();
|
||||
const { worker_id, record_date, is_present } = checkinData;
|
||||
|
||||
// 해당 날짜에 기록이 있는지 확인
|
||||
const [existing] = await db.execute(
|
||||
'SELECT id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?',
|
||||
[worker_id, record_date]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// 업데이트
|
||||
await db.execute(
|
||||
'UPDATE daily_attendance_records SET is_present = ? WHERE id = ?',
|
||||
[is_present, existing[0].id]
|
||||
);
|
||||
return existing[0].id;
|
||||
} else {
|
||||
// 새로 생성 (기본값으로)
|
||||
const [result] = await db.execute(
|
||||
`INSERT INTO daily_attendance_records
|
||||
(worker_id, record_date, is_present, attendance_type_id, created_by)
|
||||
VALUES (?, ?, ?, 1, 1)`,
|
||||
[worker_id, record_date, is_present]
|
||||
);
|
||||
return result.insertId;
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 날짜의 출근 체크 목록 조회 (휴가 정보 포함)
|
||||
static async getCheckinList(date) {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
w.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
w.employment_status,
|
||||
COALESCE(dar.is_present, TRUE) as is_present,
|
||||
dar.id as record_id,
|
||||
vr.request_id as vacation_request_id,
|
||||
vr.status as vacation_status,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code as vacation_type_code,
|
||||
vr.days_used as vacation_days
|
||||
FROM workers w
|
||||
LEFT JOIN daily_attendance_records dar
|
||||
ON w.worker_id = dar.worker_id AND dar.record_date = ?
|
||||
LEFT JOIN vacation_requests vr
|
||||
ON w.worker_id = vr.worker_id
|
||||
AND ? BETWEEN vr.start_date AND vr.end_date
|
||||
AND vr.status = 'approved'
|
||||
LEFT JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
WHERE w.employment_status = 'employed'
|
||||
ORDER BY w.worker_name
|
||||
`;
|
||||
|
||||
const [rows] = await db.execute(query, [date, date]);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttendanceModel;
|
||||
@@ -0,0 +1,154 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
/**
|
||||
* 1. 여러 개의 이슈 보고서를 트랜잭션으로 생성합니다.
|
||||
* @param {Array<object>} reports - 생성할 보고서 데이터 배열
|
||||
* @returns {Promise<Array<number>>} - 삽입된 ID 배열
|
||||
*/
|
||||
const createMany = async (reports) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const insertedIds = [];
|
||||
const sql = `
|
||||
INSERT INTO DailyIssueReports
|
||||
(date, worker_id, project_id, start_time, end_time, issue_type_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
for (const report of reports) {
|
||||
const { date, worker_id, project_id, start_time, end_time, issue_type_id } = report;
|
||||
const [result] = await conn.query(sql, [date, worker_id, project_id, start_time, end_time, issue_type_id]);
|
||||
insertedIds.push(result.insertId);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
return insertedIds;
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error('[Model] 여러 이슈 보고서 생성 중 오류:', err);
|
||||
throw new Error('데이터베이스에 이슈 보고서를 생성하는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. 특정 날짜의 전체 이슈 목록 조회 (Promise 기반)
|
||||
*/
|
||||
const getAllByDate = async (date) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
d.id, d.date, w.worker_name, p.project_name, d.start_time, d.end_time,
|
||||
t.category, t.subcategory, d.description
|
||||
FROM DailyIssueReports d
|
||||
LEFT JOIN workers w ON d.worker_id = w.worker_id
|
||||
LEFT JOIN projects p ON d.project_id = p.project_id
|
||||
LEFT JOIN IssueTypes t ON d.issue_type_id = t.issue_type_id
|
||||
WHERE d.date = ?
|
||||
ORDER BY d.start_time ASC`,
|
||||
[date]
|
||||
);
|
||||
return rows;
|
||||
} catch (err) {
|
||||
console.error(`[Model] ${date} 이슈 목록 조회 오류:`, err);
|
||||
throw new Error('데이터베이스에서 이슈 목록을 조회하는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 3. 단일 조회 (선택사항: 컨트롤러에서 사용 중)
|
||||
*/
|
||||
const getById = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT id, date, worker_id, project_id, issue_type_id, description, created_at, start_time, end_time FROM DailyIssueReports WHERE id = ?`, [id]);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 4. 수정
|
||||
*/
|
||||
const update = async (id, data, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
for (const key in data) {
|
||||
fields.push(`${key} = ?`);
|
||||
values.push(data[key]);
|
||||
}
|
||||
|
||||
values.push(id); // 마지막에 id
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE DailyIssueReports SET ${fields.join(', ')} WHERE id = ?`,
|
||||
values
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 5. 삭제 (Promise 기반)
|
||||
*/
|
||||
const remove = async (id) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`DELETE FROM DailyIssueReports WHERE id = ?`, [id]);
|
||||
return result.affectedRows;
|
||||
} catch (err) {
|
||||
console.error(`[Model] 이슈(id: ${id}) 삭제 오류:`, err);
|
||||
throw new Error('데이터베이스에서 이슈를 삭제하는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// V1 함수들은 점진적으로 제거 예정
|
||||
const create = async (report, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
date,
|
||||
worker_id,
|
||||
project_id,
|
||||
start_time,
|
||||
end_time,
|
||||
issue_type_id,
|
||||
description = null // 선택값 처리
|
||||
} = report;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO DailyIssueReports
|
||||
(date, worker_id, project_id, start_time, end_time, issue_type_id, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[date, worker_id, project_id, start_time, end_time, issue_type_id, description]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
createMany, // 신규
|
||||
getAllByDate,
|
||||
remove,
|
||||
// 레거시 호환성을 위해 V1 함수들 임시 유지
|
||||
create: (report, callback) => createMany([report]).then(ids => callback(null, ids[0])).catch(err => callback(err)),
|
||||
getById,
|
||||
update,
|
||||
};
|
||||
1388
deploy/tkfb-package/api.hyungi.net/models/dailyWorkReportModel.js
Normal file
1388
deploy/tkfb-package/api.hyungi.net/models/dailyWorkReportModel.js
Normal file
File diff suppressed because it is too large
Load Diff
120
deploy/tkfb-package/api.hyungi.net/models/departmentModel.js
Normal file
120
deploy/tkfb-package/api.hyungi.net/models/departmentModel.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// models/departmentModel.js
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const departmentModel = {
|
||||
// 모든 부서 조회 (계층 구조 포함)
|
||||
async getAll() {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT d.*,
|
||||
p.department_name as parent_name,
|
||||
(SELECT COUNT(*) FROM workers w WHERE w.department_id = d.department_id AND w.status = 'active') as worker_count
|
||||
FROM departments d
|
||||
LEFT JOIN departments p ON d.parent_id = p.department_id
|
||||
ORDER BY d.display_order, d.department_name
|
||||
`);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 활성 부서만 조회
|
||||
async getActive() {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT d.*,
|
||||
p.department_name as parent_name,
|
||||
(SELECT COUNT(*) FROM workers w WHERE w.department_id = d.department_id AND w.status = 'active') as worker_count
|
||||
FROM departments d
|
||||
LEFT JOIN departments p ON d.parent_id = p.department_id
|
||||
WHERE d.is_active = TRUE
|
||||
ORDER BY d.display_order, d.department_name
|
||||
`);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 부서 ID로 조회
|
||||
async getById(departmentId) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT d.*,
|
||||
p.department_name as parent_name
|
||||
FROM departments d
|
||||
LEFT JOIN departments p ON d.parent_id = p.department_id
|
||||
WHERE d.department_id = ?
|
||||
`, [departmentId]);
|
||||
return rows[0];
|
||||
},
|
||||
|
||||
// 부서 생성
|
||||
async create(data) {
|
||||
const db = await getDb();
|
||||
const { department_name, parent_id, description, is_active, display_order } = data;
|
||||
const [result] = await db.query(`
|
||||
INSERT INTO departments (department_name, parent_id, description, is_active, display_order)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [department_name, parent_id || null, description || null, is_active !== false, display_order || 0]);
|
||||
return result.insertId;
|
||||
},
|
||||
|
||||
// 부서 수정
|
||||
async update(departmentId, data) {
|
||||
const db = await getDb();
|
||||
const { department_name, parent_id, description, is_active, display_order } = data;
|
||||
const [result] = await db.query(`
|
||||
UPDATE departments
|
||||
SET department_name = ?, parent_id = ?, description = ?, is_active = ?, display_order = ?
|
||||
WHERE department_id = ?
|
||||
`, [department_name, parent_id || null, description || null, is_active, display_order || 0, departmentId]);
|
||||
return result.affectedRows > 0;
|
||||
},
|
||||
|
||||
// 부서 삭제
|
||||
async delete(departmentId) {
|
||||
const db = await getDb();
|
||||
// 하위 부서가 있는지 확인
|
||||
const [children] = await db.query('SELECT COUNT(*) as count FROM departments WHERE parent_id = ?', [departmentId]);
|
||||
if (children[0].count > 0) {
|
||||
throw new Error('하위 부서가 있어 삭제할 수 없습니다.');
|
||||
}
|
||||
// 소속 작업자가 있는지 확인
|
||||
const [workers] = await db.query('SELECT COUNT(*) as count FROM workers WHERE department_id = ?', [departmentId]);
|
||||
if (workers[0].count > 0) {
|
||||
throw new Error('소속 작업자가 있어 삭제할 수 없습니다. 먼저 작업자를 다른 부서로 이동하세요.');
|
||||
}
|
||||
const [result] = await db.query('DELETE FROM departments WHERE department_id = ?', [departmentId]);
|
||||
return result.affectedRows > 0;
|
||||
},
|
||||
|
||||
// 부서별 작업자 조회
|
||||
async getWorkersByDepartment(departmentId) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT w.*, d.department_name, u.user_id, u.username
|
||||
FROM workers w
|
||||
LEFT JOIN departments d ON w.department_id = d.department_id
|
||||
LEFT JOIN users u ON u.worker_id = w.worker_id
|
||||
WHERE w.department_id = ?
|
||||
ORDER BY w.worker_name
|
||||
`, [departmentId]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 작업자 부서 변경
|
||||
async moveWorker(workerId, departmentId) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`
|
||||
UPDATE workers SET department_id = ? WHERE worker_id = ?
|
||||
`, [departmentId, workerId]);
|
||||
return result.affectedRows > 0;
|
||||
},
|
||||
|
||||
// 여러 작업자 부서 일괄 변경
|
||||
async moveWorkers(workerIds, departmentId) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`
|
||||
UPDATE workers SET department_id = ? WHERE worker_id IN (?)
|
||||
`, [departmentId, workerIds]);
|
||||
return result.affectedRows;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = departmentModel;
|
||||
949
deploy/tkfb-package/api.hyungi.net/models/equipmentModel.js
Normal file
949
deploy/tkfb-package/api.hyungi.net/models/equipmentModel.js
Normal file
@@ -0,0 +1,949 @@
|
||||
// models/equipmentModel.js
|
||||
const { getDb } = require('../dbPool');
|
||||
const notificationModel = require('./notificationModel');
|
||||
|
||||
const EquipmentModel = {
|
||||
// CREATE - 설비 생성
|
||||
create: async (equipmentData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
INSERT INTO equipments (
|
||||
equipment_code, equipment_name, equipment_type, model_name,
|
||||
manufacturer, supplier, purchase_price, installation_date, serial_number, specifications,
|
||||
status, notes, workplace_id, map_x_percent, map_y_percent,
|
||||
map_width_percent, map_height_percent
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
equipmentData.equipment_code,
|
||||
equipmentData.equipment_name,
|
||||
equipmentData.equipment_type || null,
|
||||
equipmentData.model_name || null,
|
||||
equipmentData.manufacturer || null,
|
||||
equipmentData.supplier || null,
|
||||
equipmentData.purchase_price || null,
|
||||
equipmentData.installation_date || null,
|
||||
equipmentData.serial_number || null,
|
||||
equipmentData.specifications || null,
|
||||
equipmentData.status || 'active',
|
||||
equipmentData.notes || null,
|
||||
equipmentData.workplace_id || null,
|
||||
equipmentData.map_x_percent || null,
|
||||
equipmentData.map_y_percent || null,
|
||||
equipmentData.map_width_percent || null,
|
||||
equipmentData.map_height_percent || null
|
||||
];
|
||||
|
||||
const [result] = await db.query(query, values);
|
||||
callback(null, {
|
||||
equipment_id: result.insertId,
|
||||
...equipmentData
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// READ ALL - 모든 설비 조회 (필터링 옵션 포함)
|
||||
getAll: async (filters, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
e.*,
|
||||
w.workplace_name,
|
||||
wc.category_name
|
||||
FROM equipments e
|
||||
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const values = [];
|
||||
|
||||
// 필터링: 작업장 ID
|
||||
if (filters.workplace_id) {
|
||||
query += ' AND e.workplace_id = ?';
|
||||
values.push(filters.workplace_id);
|
||||
}
|
||||
|
||||
// 필터링: 설비 유형
|
||||
if (filters.equipment_type) {
|
||||
query += ' AND e.equipment_type = ?';
|
||||
values.push(filters.equipment_type);
|
||||
}
|
||||
|
||||
// 필터링: 상태
|
||||
if (filters.status) {
|
||||
query += ' AND e.status = ?';
|
||||
values.push(filters.status);
|
||||
}
|
||||
|
||||
// 필터링: 검색어 (설비명, 설비코드)
|
||||
if (filters.search) {
|
||||
query += ' AND (e.equipment_name LIKE ? OR e.equipment_code LIKE ?)';
|
||||
const searchTerm = `%${filters.search}%`;
|
||||
values.push(searchTerm, searchTerm);
|
||||
}
|
||||
|
||||
query += ' ORDER BY e.equipment_code ASC';
|
||||
|
||||
const [rows] = await db.query(query, values);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// READ ONE - 특정 설비 조회
|
||||
getById: async (equipmentId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
e.*,
|
||||
w.workplace_name,
|
||||
wc.category_name
|
||||
FROM equipments e
|
||||
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
WHERE e.equipment_id = ?
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query, [equipmentId]);
|
||||
callback(null, rows[0]);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// READ BY WORKPLACE - 특정 작업장의 설비 조회
|
||||
getByWorkplace: async (workplaceId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT e.*
|
||||
FROM equipments e
|
||||
WHERE e.workplace_id = ?
|
||||
ORDER BY e.equipment_code ASC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query, [workplaceId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// READ ACTIVE - 활성 설비만 조회
|
||||
getActive: async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
e.*,
|
||||
w.workplace_name,
|
||||
wc.category_name
|
||||
FROM equipments e
|
||||
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
WHERE e.status = 'active'
|
||||
ORDER BY e.equipment_code ASC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// UPDATE - 설비 수정
|
||||
update: async (equipmentId, equipmentData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
UPDATE equipments SET
|
||||
equipment_code = ?,
|
||||
equipment_name = ?,
|
||||
equipment_type = ?,
|
||||
model_name = ?,
|
||||
manufacturer = ?,
|
||||
supplier = ?,
|
||||
purchase_price = ?,
|
||||
installation_date = ?,
|
||||
serial_number = ?,
|
||||
specifications = ?,
|
||||
status = ?,
|
||||
notes = ?,
|
||||
workplace_id = ?,
|
||||
map_x_percent = ?,
|
||||
map_y_percent = ?,
|
||||
map_width_percent = ?,
|
||||
map_height_percent = ?,
|
||||
updated_at = NOW()
|
||||
WHERE equipment_id = ?
|
||||
`;
|
||||
|
||||
const values = [
|
||||
equipmentData.equipment_code,
|
||||
equipmentData.equipment_name,
|
||||
equipmentData.equipment_type || null,
|
||||
equipmentData.model_name || null,
|
||||
equipmentData.manufacturer || null,
|
||||
equipmentData.supplier || null,
|
||||
equipmentData.purchase_price || null,
|
||||
equipmentData.installation_date || null,
|
||||
equipmentData.serial_number || null,
|
||||
equipmentData.specifications || null,
|
||||
equipmentData.status || 'active',
|
||||
equipmentData.notes || null,
|
||||
equipmentData.workplace_id || null,
|
||||
equipmentData.map_x_percent || null,
|
||||
equipmentData.map_y_percent || null,
|
||||
equipmentData.map_width_percent || null,
|
||||
equipmentData.map_height_percent || null,
|
||||
equipmentId
|
||||
];
|
||||
|
||||
const [result] = await db.query(query, values);
|
||||
if (result.affectedRows === 0) {
|
||||
return callback(new Error('Equipment not found'));
|
||||
}
|
||||
callback(null, { equipment_id: equipmentId, ...equipmentData });
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// UPDATE MAP POSITION - 지도상 위치 업데이트 (선택적으로 workplace_id도 업데이트)
|
||||
updateMapPosition: async (equipmentId, positionData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// workplace_id가 포함된 경우 함께 업데이트
|
||||
const hasWorkplaceId = positionData.workplace_id !== undefined;
|
||||
|
||||
const query = hasWorkplaceId ? `
|
||||
UPDATE equipments SET
|
||||
workplace_id = ?,
|
||||
map_x_percent = ?,
|
||||
map_y_percent = ?,
|
||||
map_width_percent = ?,
|
||||
map_height_percent = ?,
|
||||
updated_at = NOW()
|
||||
WHERE equipment_id = ?
|
||||
` : `
|
||||
UPDATE equipments SET
|
||||
map_x_percent = ?,
|
||||
map_y_percent = ?,
|
||||
map_width_percent = ?,
|
||||
map_height_percent = ?,
|
||||
updated_at = NOW()
|
||||
WHERE equipment_id = ?
|
||||
`;
|
||||
|
||||
const values = hasWorkplaceId ? [
|
||||
positionData.workplace_id,
|
||||
positionData.map_x_percent,
|
||||
positionData.map_y_percent,
|
||||
positionData.map_width_percent,
|
||||
positionData.map_height_percent,
|
||||
equipmentId
|
||||
] : [
|
||||
positionData.map_x_percent,
|
||||
positionData.map_y_percent,
|
||||
positionData.map_width_percent,
|
||||
positionData.map_height_percent,
|
||||
equipmentId
|
||||
];
|
||||
|
||||
const [result] = await db.query(query, values);
|
||||
if (result.affectedRows === 0) {
|
||||
return callback(new Error('Equipment not found'));
|
||||
}
|
||||
callback(null, { equipment_id: equipmentId, ...positionData });
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE - 설비 삭제
|
||||
delete: async (equipmentId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = 'DELETE FROM equipments WHERE equipment_id = ?';
|
||||
|
||||
const [result] = await db.query(query, [equipmentId]);
|
||||
if (result.affectedRows === 0) {
|
||||
return callback(new Error('Equipment not found'));
|
||||
}
|
||||
callback(null, { equipment_id: equipmentId });
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// CHECK DUPLICATE CODE - 설비 코드 중복 확인
|
||||
checkDuplicateCode: async (equipmentCode, excludeEquipmentId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = 'SELECT equipment_id FROM equipments WHERE equipment_code = ?';
|
||||
const values = [equipmentCode];
|
||||
|
||||
if (excludeEquipmentId) {
|
||||
query += ' AND equipment_id != ?';
|
||||
values.push(excludeEquipmentId);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(query, values);
|
||||
callback(null, rows.length > 0);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET EQUIPMENT TYPES - 사용 중인 설비 유형 목록 조회
|
||||
getEquipmentTypes: async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT DISTINCT equipment_type
|
||||
FROM equipments
|
||||
WHERE equipment_type IS NOT NULL
|
||||
ORDER BY equipment_type ASC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows.map(row => row.equipment_type));
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET NEXT EQUIPMENT CODE - 다음 관리번호 자동 생성 (TKP-001 형식)
|
||||
getNextEquipmentCode: async (prefix = 'TKP', callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
// 해당 접두사로 시작하는 가장 큰 번호 찾기
|
||||
const query = `
|
||||
SELECT equipment_code
|
||||
FROM equipments
|
||||
WHERE equipment_code LIKE ?
|
||||
ORDER BY equipment_code DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query, [`${prefix}-%`]);
|
||||
|
||||
let nextNumber = 1;
|
||||
if (rows.length > 0) {
|
||||
// TKP-001 형식에서 숫자 부분 추출
|
||||
const lastCode = rows[0].equipment_code;
|
||||
const match = lastCode.match(new RegExp(`^${prefix}-(\\d+)$`));
|
||||
if (match) {
|
||||
nextNumber = parseInt(match[1], 10) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 3자리로 패딩 (001, 002, ...)
|
||||
const nextCode = `${prefix}-${String(nextNumber).padStart(3, '0')}`;
|
||||
callback(null, nextCode);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 사진 관리
|
||||
// ==========================================
|
||||
|
||||
// ADD PHOTO - 설비 사진 추가
|
||||
addPhoto: async (equipmentId, photoData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
INSERT INTO equipment_photos (
|
||||
equipment_id, photo_path, description, display_order, uploaded_by
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
equipmentId,
|
||||
photoData.photo_path,
|
||||
photoData.description || null,
|
||||
photoData.display_order || 0,
|
||||
photoData.uploaded_by || null
|
||||
];
|
||||
|
||||
const [result] = await db.query(query, values);
|
||||
callback(null, {
|
||||
photo_id: result.insertId,
|
||||
equipment_id: equipmentId,
|
||||
...photoData
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET PHOTOS - 설비 사진 조회
|
||||
getPhotos: async (equipmentId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT ep.*, u.name AS uploaded_by_name
|
||||
FROM equipment_photos ep
|
||||
LEFT JOIN users u ON ep.uploaded_by = u.user_id
|
||||
WHERE ep.equipment_id = ?
|
||||
ORDER BY ep.display_order ASC, ep.created_at ASC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query, [equipmentId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE PHOTO - 설비 사진 삭제
|
||||
deletePhoto: async (photoId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 먼저 사진 정보 조회 (파일 삭제용)
|
||||
const [photo] = await db.query(
|
||||
'SELECT photo_path FROM equipment_photos WHERE photo_id = ?',
|
||||
[photoId]
|
||||
);
|
||||
|
||||
const query = 'DELETE FROM equipment_photos WHERE photo_id = ?';
|
||||
const [result] = await db.query(query, [photoId]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return callback(new Error('Photo not found'));
|
||||
}
|
||||
|
||||
callback(null, { photo_id: photoId, photo_path: photo[0]?.photo_path });
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 임시 이동
|
||||
// ==========================================
|
||||
|
||||
// MOVE TEMPORARILY - 설비 임시 이동
|
||||
moveTemporarily: async (equipmentId, moveData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 설비 현재 위치 업데이트
|
||||
const updateQuery = `
|
||||
UPDATE equipments SET
|
||||
current_workplace_id = ?,
|
||||
current_map_x_percent = ?,
|
||||
current_map_y_percent = ?,
|
||||
current_map_width_percent = ?,
|
||||
current_map_height_percent = ?,
|
||||
is_temporarily_moved = TRUE,
|
||||
moved_at = NOW(),
|
||||
moved_by = ?,
|
||||
updated_at = NOW()
|
||||
WHERE equipment_id = ?
|
||||
`;
|
||||
|
||||
const updateValues = [
|
||||
moveData.target_workplace_id,
|
||||
moveData.target_x_percent,
|
||||
moveData.target_y_percent,
|
||||
moveData.target_width_percent || null,
|
||||
moveData.target_height_percent || null,
|
||||
moveData.moved_by || null,
|
||||
equipmentId
|
||||
];
|
||||
|
||||
const [result] = await db.query(updateQuery, updateValues);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return callback(new Error('Equipment not found'));
|
||||
}
|
||||
|
||||
// 2. 이동 이력 기록
|
||||
const logQuery = `
|
||||
INSERT INTO equipment_move_logs (
|
||||
equipment_id, move_type,
|
||||
from_workplace_id, to_workplace_id,
|
||||
from_x_percent, from_y_percent,
|
||||
to_x_percent, to_y_percent,
|
||||
reason, moved_by
|
||||
) VALUES (?, 'temporary', ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
await db.query(logQuery, [
|
||||
equipmentId,
|
||||
moveData.from_workplace_id || null,
|
||||
moveData.target_workplace_id,
|
||||
moveData.from_x_percent || null,
|
||||
moveData.from_y_percent || null,
|
||||
moveData.target_x_percent,
|
||||
moveData.target_y_percent,
|
||||
moveData.reason || null,
|
||||
moveData.moved_by || null
|
||||
]);
|
||||
|
||||
callback(null, { equipment_id: equipmentId, moved: true });
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// RETURN TO ORIGINAL - 설비 원위치 복귀
|
||||
returnToOriginal: async (equipmentId, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 현재 임시 위치 정보 조회
|
||||
const [equipment] = await db.query(
|
||||
'SELECT current_workplace_id, current_map_x_percent, current_map_y_percent FROM equipments WHERE equipment_id = ?',
|
||||
[equipmentId]
|
||||
);
|
||||
|
||||
if (!equipment[0]) {
|
||||
return callback(new Error('Equipment not found'));
|
||||
}
|
||||
|
||||
// 2. 임시 위치 필드 초기화
|
||||
const updateQuery = `
|
||||
UPDATE equipments SET
|
||||
current_workplace_id = NULL,
|
||||
current_map_x_percent = NULL,
|
||||
current_map_y_percent = NULL,
|
||||
current_map_width_percent = NULL,
|
||||
current_map_height_percent = NULL,
|
||||
is_temporarily_moved = FALSE,
|
||||
moved_at = NULL,
|
||||
moved_by = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE equipment_id = ?
|
||||
`;
|
||||
|
||||
await db.query(updateQuery, [equipmentId]);
|
||||
|
||||
// 3. 복귀 이력 기록
|
||||
const logQuery = `
|
||||
INSERT INTO equipment_move_logs (
|
||||
equipment_id, move_type,
|
||||
from_workplace_id, from_x_percent, from_y_percent,
|
||||
reason, moved_by
|
||||
) VALUES (?, 'return', ?, ?, ?, '원위치 복귀', ?)
|
||||
`;
|
||||
|
||||
await db.query(logQuery, [
|
||||
equipmentId,
|
||||
equipment[0].current_workplace_id,
|
||||
equipment[0].current_map_x_percent,
|
||||
equipment[0].current_map_y_percent,
|
||||
userId || null
|
||||
]);
|
||||
|
||||
callback(null, { equipment_id: equipmentId, returned: true });
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET TEMPORARILY MOVED - 임시 이동된 설비 목록
|
||||
getTemporarilyMoved: async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
e.*,
|
||||
w_orig.workplace_name AS original_workplace_name,
|
||||
w_curr.workplace_name AS current_workplace_name,
|
||||
u.name AS moved_by_name
|
||||
FROM equipments e
|
||||
LEFT JOIN workplaces w_orig ON e.workplace_id = w_orig.workplace_id
|
||||
LEFT JOIN workplaces w_curr ON e.current_workplace_id = w_curr.workplace_id
|
||||
LEFT JOIN users u ON e.moved_by = u.user_id
|
||||
WHERE e.is_temporarily_moved = TRUE
|
||||
ORDER BY e.moved_at DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET MOVE LOGS - 설비 이동 이력 조회
|
||||
getMoveLogs: async (equipmentId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
eml.*,
|
||||
w_from.workplace_name AS from_workplace_name,
|
||||
w_to.workplace_name AS to_workplace_name,
|
||||
u.name AS moved_by_name
|
||||
FROM equipment_move_logs eml
|
||||
LEFT JOIN workplaces w_from ON eml.from_workplace_id = w_from.workplace_id
|
||||
LEFT JOIN workplaces w_to ON eml.to_workplace_id = w_to.workplace_id
|
||||
LEFT JOIN users u ON eml.moved_by = u.user_id
|
||||
WHERE eml.equipment_id = ?
|
||||
ORDER BY eml.moved_at DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query, [equipmentId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 외부 반출/반입
|
||||
// ==========================================
|
||||
|
||||
// EXPORT EQUIPMENT - 설비 외부 반출
|
||||
exportEquipment: async (exportData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 반출 로그 생성
|
||||
const logQuery = `
|
||||
INSERT INTO equipment_external_logs (
|
||||
equipment_id, log_type, export_date, expected_return_date,
|
||||
destination, reason, notes, exported_by
|
||||
) VALUES (?, 'export', ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const logValues = [
|
||||
exportData.equipment_id,
|
||||
exportData.export_date || new Date().toISOString().slice(0, 10),
|
||||
exportData.expected_return_date || null,
|
||||
exportData.destination || null,
|
||||
exportData.reason || null,
|
||||
exportData.notes || null,
|
||||
exportData.exported_by || null
|
||||
];
|
||||
|
||||
const [logResult] = await db.query(logQuery, logValues);
|
||||
|
||||
// 2. 설비 상태 업데이트
|
||||
const status = exportData.is_repair ? 'repair_external' : 'external';
|
||||
await db.query(
|
||||
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
|
||||
[status, exportData.equipment_id]
|
||||
);
|
||||
|
||||
callback(null, {
|
||||
log_id: logResult.insertId,
|
||||
equipment_id: exportData.equipment_id,
|
||||
exported: true
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// RETURN EQUIPMENT - 설비 반입 (외부에서 복귀)
|
||||
returnEquipment: async (logId, returnData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 반출 로그 조회
|
||||
const [logs] = await db.query(
|
||||
'SELECT equipment_id FROM equipment_external_logs WHERE log_id = ?',
|
||||
[logId]
|
||||
);
|
||||
|
||||
if (!logs[0]) {
|
||||
return callback(new Error('Export log not found'));
|
||||
}
|
||||
|
||||
const equipmentId = logs[0].equipment_id;
|
||||
|
||||
// 2. 반출 로그 업데이트
|
||||
await db.query(
|
||||
`UPDATE equipment_external_logs SET
|
||||
actual_return_date = ?,
|
||||
returned_by = ?,
|
||||
notes = CONCAT(IFNULL(notes, ''), '\n반입: ', IFNULL(?, '')),
|
||||
updated_at = NOW()
|
||||
WHERE log_id = ?`,
|
||||
[
|
||||
returnData.return_date || new Date().toISOString().slice(0, 10),
|
||||
returnData.returned_by || null,
|
||||
returnData.notes || '',
|
||||
logId
|
||||
]
|
||||
);
|
||||
|
||||
// 3. 설비 상태 복원
|
||||
await db.query(
|
||||
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
|
||||
[returnData.new_status || 'active', equipmentId]
|
||||
);
|
||||
|
||||
callback(null, {
|
||||
log_id: logId,
|
||||
equipment_id: equipmentId,
|
||||
returned: true
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET EXTERNAL LOGS - 설비 외부 반출 이력 조회
|
||||
getExternalLogs: async (equipmentId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
eel.*,
|
||||
u_exp.name AS exported_by_name,
|
||||
u_ret.name AS returned_by_name
|
||||
FROM equipment_external_logs eel
|
||||
LEFT JOIN users u_exp ON eel.exported_by = u_exp.user_id
|
||||
LEFT JOIN users u_ret ON eel.returned_by = u_ret.user_id
|
||||
WHERE eel.equipment_id = ?
|
||||
ORDER BY eel.created_at DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query, [equipmentId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET EXPORTED EQUIPMENTS - 현재 외부 반출 중인 설비 목록
|
||||
getExportedEquipments: async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
e.*,
|
||||
w.workplace_name,
|
||||
eel.export_date,
|
||||
eel.expected_return_date,
|
||||
eel.destination,
|
||||
eel.reason,
|
||||
u.name AS exported_by_name
|
||||
FROM equipments e
|
||||
INNER JOIN (
|
||||
SELECT equipment_id, MAX(log_id) AS latest_log_id
|
||||
FROM equipment_external_logs
|
||||
WHERE actual_return_date IS NULL
|
||||
GROUP BY equipment_id
|
||||
) latest ON e.equipment_id = latest.equipment_id
|
||||
INNER JOIN equipment_external_logs eel ON eel.log_id = latest.latest_log_id
|
||||
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
|
||||
LEFT JOIN users u ON eel.exported_by = u.user_id
|
||||
WHERE e.status IN ('external', 'repair_external')
|
||||
ORDER BY eel.export_date DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 수리 신청 (work_issue_reports 연동)
|
||||
// ==========================================
|
||||
|
||||
// CREATE REPAIR REQUEST - 수리 신청 (신고 시스템 활용)
|
||||
createRepairRequest: async (requestData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 설비 수리 카테고리 ID 조회
|
||||
const [categories] = await db.query(
|
||||
"SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리' LIMIT 1"
|
||||
);
|
||||
|
||||
if (!categories[0]) {
|
||||
return callback(new Error('설비 수리 카테고리가 없습니다'));
|
||||
}
|
||||
|
||||
const categoryId = categories[0].category_id;
|
||||
|
||||
// 항목 ID 조회 (지정된 항목이 없으면 첫번째 항목 사용)
|
||||
let itemId = requestData.item_id;
|
||||
if (!itemId) {
|
||||
const [items] = await db.query(
|
||||
'SELECT item_id FROM issue_report_items WHERE category_id = ? LIMIT 1',
|
||||
[categoryId]
|
||||
);
|
||||
itemId = items[0]?.item_id;
|
||||
}
|
||||
|
||||
// 사진 경로 분리 (최대 5장)
|
||||
const photos = requestData.photo_paths || [];
|
||||
|
||||
// work_issue_reports에 삽입
|
||||
const query = `
|
||||
INSERT INTO work_issue_reports (
|
||||
reporter_id, issue_category_id, issue_item_id,
|
||||
workplace_id, equipment_id,
|
||||
additional_description,
|
||||
photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
|
||||
status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'reported')
|
||||
`;
|
||||
|
||||
const values = [
|
||||
requestData.reported_by || null,
|
||||
categoryId,
|
||||
itemId,
|
||||
requestData.workplace_id || null,
|
||||
requestData.equipment_id,
|
||||
requestData.description || null,
|
||||
photos[0] || null,
|
||||
photos[1] || null,
|
||||
photos[2] || null,
|
||||
photos[3] || null,
|
||||
photos[4] || null
|
||||
];
|
||||
|
||||
const [result] = await db.query(query, values);
|
||||
|
||||
// 설비 상태를 repair_needed로 업데이트
|
||||
await db.query(
|
||||
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
|
||||
['repair_needed', requestData.equipment_id]
|
||||
);
|
||||
|
||||
// 알림 생성
|
||||
try {
|
||||
await notificationModel.createRepairNotification({
|
||||
equipment_id: requestData.equipment_id,
|
||||
equipment_name: requestData.equipment_name || '설비',
|
||||
repair_type: requestData.repair_type || '일반 수리',
|
||||
request_id: result.insertId,
|
||||
created_by: requestData.reported_by
|
||||
});
|
||||
} catch (notifError) {
|
||||
console.error('알림 생성 실패:', notifError);
|
||||
// 알림 생성 실패해도 수리 신청은 성공으로 처리
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
report_id: result.insertId,
|
||||
equipment_id: requestData.equipment_id,
|
||||
created: true
|
||||
});
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET REPAIR HISTORY - 설비 수리 이력 조회
|
||||
getRepairHistory: async (equipmentId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
wir.*,
|
||||
irc.category_name,
|
||||
iri.item_name,
|
||||
u_rep.name AS reported_by_name,
|
||||
u_res.name AS resolved_by_name,
|
||||
w.workplace_name
|
||||
FROM work_issue_reports wir
|
||||
LEFT JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
|
||||
LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id
|
||||
LEFT JOIN users u_rep ON wir.reporter_id = u_rep.user_id
|
||||
LEFT JOIN users u_res ON wir.resolved_by = u_res.user_id
|
||||
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
|
||||
WHERE wir.equipment_id = ?
|
||||
ORDER BY wir.created_at DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query, [equipmentId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// GET REPAIR CATEGORIES - 설비 수리 항목 목록 조회
|
||||
getRepairCategories: async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 설비 수리 카테고리의 항목들 조회
|
||||
const query = `
|
||||
SELECT iri.item_id, iri.item_name, iri.description, iri.severity
|
||||
FROM issue_report_items iri
|
||||
INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
WHERE irc.category_name = '설비 수리' AND iri.is_active = 1
|
||||
ORDER BY iri.display_order ASC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
// ADD REPAIR CATEGORY - 새 수리 항목 추가
|
||||
addRepairCategory: async (itemName, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 설비 수리 카테고리 ID 조회
|
||||
const [categories] = await db.query(
|
||||
"SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리'"
|
||||
);
|
||||
|
||||
if (categories.length === 0) {
|
||||
return callback(new Error('설비 수리 카테고리가 없습니다.'));
|
||||
}
|
||||
|
||||
const categoryId = categories[0].category_id;
|
||||
|
||||
// 중복 확인
|
||||
const [existing] = await db.query(
|
||||
'SELECT item_id FROM issue_report_items WHERE category_id = ? AND item_name = ?',
|
||||
[categoryId, itemName]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// 이미 존재하면 해당 ID 반환
|
||||
return callback(null, { item_id: existing[0].item_id, item_name: itemName, isNew: false });
|
||||
}
|
||||
|
||||
// 다음 display_order 구하기
|
||||
const [maxOrder] = await db.query(
|
||||
'SELECT MAX(display_order) as max_order FROM issue_report_items WHERE category_id = ?',
|
||||
[categoryId]
|
||||
);
|
||||
const nextOrder = (maxOrder[0].max_order || 0) + 1;
|
||||
|
||||
// 새 항목 추가
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO issue_report_items (category_id, item_name, display_order, is_active)
|
||||
VALUES (?, ?, ?, 1)`,
|
||||
[categoryId, itemName, nextOrder]
|
||||
);
|
||||
|
||||
callback(null, { item_id: result.insertId, item_name: itemName, isNew: true });
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EquipmentModel;
|
||||
42
deploy/tkfb-package/api.hyungi.net/models/issueTypeModel.js
Normal file
42
deploy/tkfb-package/api.hyungi.net/models/issueTypeModel.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// CREATE
|
||||
const create = async (type) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO IssueTypes (category, subcategory) VALUES (?, ?)`,
|
||||
[type.category, type.subcategory]
|
||||
);
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
// READ ALL
|
||||
const getAll = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT issue_type_id, category, subcategory FROM IssueTypes ORDER BY category, subcategory`);
|
||||
return rows;
|
||||
};
|
||||
|
||||
// UPDATE
|
||||
const update = async (id, type) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE IssueTypes SET category = ?, subcategory = ? WHERE id = ?`,
|
||||
[type.category, type.subcategory, id]
|
||||
);
|
||||
return result.affectedRows;
|
||||
};
|
||||
|
||||
// DELETE
|
||||
const remove = async (id) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`DELETE FROM IssueTypes WHERE id = ?`, [id]);
|
||||
return result.affectedRows;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
183
deploy/tkfb-package/api.hyungi.net/models/monthlyStatusModel.js
Normal file
183
deploy/tkfb-package/api.hyungi.net/models/monthlyStatusModel.js
Normal file
@@ -0,0 +1,183 @@
|
||||
// models/monthlyStatusModel.js
|
||||
// 월별 작업자 상태 집계 모델
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
class MonthlyStatusModel {
|
||||
// 월별 일자별 요약 조회 (캘린더용)
|
||||
static async getMonthlySummary(year, month) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
const [rows] = await db.execute(`
|
||||
SELECT
|
||||
date,
|
||||
total_workers,
|
||||
working_workers,
|
||||
incomplete_workers,
|
||||
partial_workers,
|
||||
complete_workers,
|
||||
overtime_workers,
|
||||
vacation_workers,
|
||||
error_workers,
|
||||
overtime_warning_workers,
|
||||
total_work_hours,
|
||||
total_work_count,
|
||||
total_error_count,
|
||||
has_issues,
|
||||
has_errors,
|
||||
has_overtime_warning,
|
||||
last_updated
|
||||
FROM monthly_summary
|
||||
WHERE year = ? AND month = ?
|
||||
ORDER BY date ASC
|
||||
`, [year, month]);
|
||||
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('월별 요약 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 날짜의 작업자별 상태 조회 (모달용)
|
||||
// ✅ 리팩토링: 집계 테이블 대신 daily_work_reports에서 직접 조회 (중복 문제 완전 해결)
|
||||
static async getDailyWorkerStatus(date) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// daily_work_reports에서 직접 집계하여 조회 (중복 없음 보장)
|
||||
const [rows] = await db.query(`
|
||||
SELECT
|
||||
w.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
YEAR(?) as year,
|
||||
MONTH(?) as month,
|
||||
? as date,
|
||||
COALESCE(SUM(dwr.work_hours), 0) as total_work_hours,
|
||||
COALESCE(SUM(CASE WHEN dwr.project_id != 13 THEN dwr.work_hours ELSE 0 END), 0) as actual_work_hours,
|
||||
COALESCE(SUM(CASE WHEN dwr.project_id = 13 THEN dwr.work_hours ELSE 0 END), 0) as vacation_hours,
|
||||
COUNT(dwr.id) as total_work_count,
|
||||
COUNT(CASE WHEN dwr.project_id != 13 AND dwr.work_status_id != 2 THEN 1 END) as regular_work_count,
|
||||
COUNT(CASE WHEN dwr.work_status_id = 2 THEN 1 END) as error_work_count,
|
||||
CASE
|
||||
WHEN MAX(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) = 1 THEN 'error'
|
||||
WHEN SUM(dwr.work_hours) > 12 THEN 'overtime-warning'
|
||||
WHEN SUM(dwr.work_hours) > 8 THEN 'overtime'
|
||||
WHEN SUM(dwr.work_hours) = 8 THEN 'complete'
|
||||
WHEN SUM(dwr.work_hours) > 0 THEN 'partial'
|
||||
ELSE 'incomplete'
|
||||
END as work_status,
|
||||
MAX(CASE WHEN dwr.project_id = 13 THEN 1 ELSE 0 END) as has_vacation,
|
||||
MAX(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as has_error,
|
||||
CASE
|
||||
WHEN SUM(dwr.work_hours) < 8 AND SUM(dwr.work_hours) > 0 THEN 1
|
||||
ELSE 0
|
||||
END as has_issues,
|
||||
MAX(dwr.created_at) as last_updated
|
||||
FROM workers w
|
||||
LEFT JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id AND dwr.report_date = ?
|
||||
WHERE w.status = 'active'
|
||||
GROUP BY w.worker_id, w.worker_name, w.job_type
|
||||
ORDER BY w.worker_name ASC
|
||||
`, [date, date, date, date]);
|
||||
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('일별 작업자 상태 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 월별 집계 데이터 강제 재계산 (관리용)
|
||||
static async recalculateMonth(year, month) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 해당 월의 모든 날짜와 작업자 조합을 찾아서 재계산
|
||||
const [workDates] = await db.execute(`
|
||||
SELECT DISTINCT report_date, worker_id
|
||||
FROM daily_work_reports
|
||||
WHERE YEAR(report_date) = ? AND MONTH(report_date) = ?
|
||||
`, [year, month]);
|
||||
|
||||
let updatedCount = 0;
|
||||
|
||||
for (const { report_date, worker_id } of workDates) {
|
||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [report_date, worker_id]);
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
console.log(`✅ ${year}년 ${month}월 집계 재계산 완료: ${updatedCount}건`);
|
||||
return { success: true, updatedCount };
|
||||
|
||||
} catch (error) {
|
||||
console.error('월별 집계 재계산 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 날짜 집계 강제 업데이트
|
||||
static async updateDateSummary(date, workerId = null) {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
if (workerId) {
|
||||
// 특정 작업자만 업데이트
|
||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, workerId]);
|
||||
} else {
|
||||
// 해당 날짜의 모든 작업자 업데이트
|
||||
const [workers] = await db.execute(`
|
||||
SELECT DISTINCT worker_id
|
||||
FROM daily_work_reports
|
||||
WHERE report_date = ?
|
||||
`, [date]);
|
||||
|
||||
for (const { worker_id } of workers) {
|
||||
await db.execute('CALL UpdateMonthlyWorkerStatus(?, ?)', [date, worker_id]);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('날짜별 집계 업데이트 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 집계 테이블 상태 확인
|
||||
static async getStatusInfo() {
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
const [summaryCount] = await db.execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_days,
|
||||
MIN(date) as earliest_date,
|
||||
MAX(date) as latest_date,
|
||||
MAX(last_updated) as last_update
|
||||
FROM monthly_summary
|
||||
`);
|
||||
|
||||
const [workerStatusCount] = await db.execute(`
|
||||
SELECT
|
||||
COUNT(*) as total_records,
|
||||
COUNT(DISTINCT worker_id) as unique_workers,
|
||||
COUNT(DISTINCT date) as unique_dates,
|
||||
MAX(last_updated) as last_update
|
||||
FROM monthly_worker_status
|
||||
`);
|
||||
|
||||
return {
|
||||
summary: summaryCount[0],
|
||||
workerStatus: workerStatusCount[0]
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('집계 테이블 상태 확인 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MonthlyStatusModel;
|
||||
197
deploy/tkfb-package/api.hyungi.net/models/notificationModel.js
Normal file
197
deploy/tkfb-package/api.hyungi.net/models/notificationModel.js
Normal file
@@ -0,0 +1,197 @@
|
||||
// models/notificationModel.js
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 순환 참조를 피하기 위해 함수 내에서 require
|
||||
async function getRecipientIds(notificationType) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT user_id FROM notification_recipients
|
||||
WHERE notification_type = ? AND is_active = 1`,
|
||||
[notificationType]
|
||||
);
|
||||
return rows.map(r => r.user_id);
|
||||
}
|
||||
|
||||
const notificationModel = {
|
||||
// 알림 생성
|
||||
async create(notificationData) {
|
||||
const db = await getDb();
|
||||
const { user_id, type, title, message, link_url, reference_type, reference_id, created_by } = notificationData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO notifications (user_id, type, title, message, link_url, reference_type, reference_id, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[user_id || null, type || 'system', title, message || null, link_url || null, reference_type || null, reference_id || null, created_by || null]
|
||||
);
|
||||
|
||||
return result.insertId;
|
||||
},
|
||||
|
||||
// 읽지 않은 알림 조회 (특정 사용자 또는 전체)
|
||||
async getUnread(userId = null) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM notifications
|
||||
WHERE is_read = 0
|
||||
AND (user_id IS NULL OR user_id = ?)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50`,
|
||||
[userId || 0]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 전체 알림 조회 (페이징)
|
||||
async getAll(userId = null, page = 1, limit = 20) {
|
||||
const db = await getDb();
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM notifications
|
||||
WHERE (user_id IS NULL OR user_id = ?)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[userId || 0, limit, offset]
|
||||
);
|
||||
|
||||
const [[{ total }]] = await db.query(
|
||||
`SELECT COUNT(*) as total FROM notifications
|
||||
WHERE (user_id IS NULL OR user_id = ?)`,
|
||||
[userId || 0]
|
||||
);
|
||||
|
||||
return { notifications: rows, total, page, limit };
|
||||
},
|
||||
|
||||
// 알림 읽음 처리
|
||||
async markAsRead(notificationId) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE notifications SET is_read = 1, read_at = NOW() WHERE notification_id = ?`,
|
||||
[notificationId]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
},
|
||||
|
||||
// 모든 알림 읽음 처리
|
||||
async markAllAsRead(userId = null) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE notifications SET is_read = 1, read_at = NOW()
|
||||
WHERE is_read = 0 AND (user_id IS NULL OR user_id = ?)`,
|
||||
[userId || 0]
|
||||
);
|
||||
return result.affectedRows;
|
||||
},
|
||||
|
||||
// 알림 삭제
|
||||
async delete(notificationId) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM notifications WHERE notification_id = ?`,
|
||||
[notificationId]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
},
|
||||
|
||||
// 오래된 알림 삭제 (30일 이상)
|
||||
async deleteOld(days = 30) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM notifications WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)`,
|
||||
[days]
|
||||
);
|
||||
return result.affectedRows;
|
||||
},
|
||||
|
||||
// 읽지 않은 알림 개수
|
||||
async getUnreadCount(userId = null) {
|
||||
const db = await getDb();
|
||||
const [[{ count }]] = await db.query(
|
||||
`SELECT COUNT(*) as count FROM notifications
|
||||
WHERE is_read = 0 AND (user_id IS NULL OR user_id = ?)`,
|
||||
[userId || 0]
|
||||
);
|
||||
return count;
|
||||
},
|
||||
|
||||
// 수리 신청 알림 생성 헬퍼 (지정된 수신자에게 전송)
|
||||
async createRepairNotification(repairData) {
|
||||
const { equipment_id, equipment_name, repair_type, request_id, created_by } = repairData;
|
||||
|
||||
// 수리 알림 수신자 목록 가져오기
|
||||
const recipientIds = await getRecipientIds('repair');
|
||||
|
||||
if (recipientIds.length === 0) {
|
||||
// 수신자가 지정되지 않은 경우 전체 알림 (user_id = null)
|
||||
return await this.create({
|
||||
type: 'repair',
|
||||
title: `수리 신청: ${equipment_name || '설비'}`,
|
||||
message: `${repair_type} 수리가 신청되었습니다.`,
|
||||
link_url: `/pages/admin/repair-management.html`,
|
||||
reference_type: 'work_issue_reports',
|
||||
reference_id: request_id,
|
||||
created_by
|
||||
});
|
||||
}
|
||||
|
||||
// 지정된 수신자 각각에게 알림 생성
|
||||
const results = [];
|
||||
for (const userId of recipientIds) {
|
||||
const notificationId = await this.create({
|
||||
user_id: userId,
|
||||
type: 'repair',
|
||||
title: `수리 신청: ${equipment_name || '설비'}`,
|
||||
message: `${repair_type} 수리가 신청되었습니다.`,
|
||||
link_url: `/pages/admin/repair-management.html`,
|
||||
reference_type: 'work_issue_reports',
|
||||
reference_id: request_id,
|
||||
created_by
|
||||
});
|
||||
results.push(notificationId);
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
|
||||
// 일반 알림 생성 (유형별 지정된 수신자에게 전송)
|
||||
async createTypedNotification(notificationData) {
|
||||
const { type, title, message, link_url, reference_type, reference_id, created_by } = notificationData;
|
||||
|
||||
// 해당 유형의 수신자 목록 가져오기
|
||||
const recipientIds = await getRecipientIds(type);
|
||||
|
||||
if (recipientIds.length === 0) {
|
||||
// 수신자가 지정되지 않은 경우 전체 알림
|
||||
return await this.create({
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
link_url,
|
||||
reference_type,
|
||||
reference_id,
|
||||
created_by
|
||||
});
|
||||
}
|
||||
|
||||
// 지정된 수신자 각각에게 알림 생성
|
||||
const results = [];
|
||||
for (const userId of recipientIds) {
|
||||
const notificationId = await this.create({
|
||||
user_id: userId,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
link_url,
|
||||
reference_type,
|
||||
reference_id,
|
||||
created_by
|
||||
});
|
||||
results.push(notificationId);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = notificationModel;
|
||||
@@ -0,0 +1,146 @@
|
||||
// models/notificationRecipientModel.js
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const NOTIFICATION_TYPES = {
|
||||
repair: '설비 수리',
|
||||
safety: '안전 신고',
|
||||
nonconformity: '부적합 신고',
|
||||
equipment: '설비 관련',
|
||||
maintenance: '정기점검',
|
||||
system: '시스템'
|
||||
};
|
||||
|
||||
const notificationRecipientModel = {
|
||||
// 알림 유형 목록 가져오기
|
||||
getTypes() {
|
||||
return NOTIFICATION_TYPES;
|
||||
},
|
||||
|
||||
// 유형별 수신자 목록 조회
|
||||
async getByType(notificationType) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT nr.*, u.username, u.name as user_name, r.name as role
|
||||
FROM notification_recipients nr
|
||||
JOIN users u ON nr.user_id = u.user_id
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
WHERE nr.notification_type = ? AND nr.is_active = 1
|
||||
ORDER BY u.name`,
|
||||
[notificationType]
|
||||
);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 전체 수신자 목록 조회 (유형별 그룹화)
|
||||
async getAll() {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT nr.*, u.username, u.name as user_name, r.name as role
|
||||
FROM notification_recipients nr
|
||||
JOIN users u ON nr.user_id = u.user_id
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
WHERE nr.is_active = 1
|
||||
ORDER BY nr.notification_type, u.name`
|
||||
);
|
||||
|
||||
// 유형별로 그룹화
|
||||
const grouped = {};
|
||||
for (const type in NOTIFICATION_TYPES) {
|
||||
grouped[type] = {
|
||||
label: NOTIFICATION_TYPES[type],
|
||||
recipients: []
|
||||
};
|
||||
}
|
||||
|
||||
rows.forEach(row => {
|
||||
if (grouped[row.notification_type]) {
|
||||
grouped[row.notification_type].recipients.push(row);
|
||||
}
|
||||
});
|
||||
|
||||
return grouped;
|
||||
},
|
||||
|
||||
// 수신자 추가
|
||||
async add(notificationType, userId, createdBy = null) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO notification_recipients (notification_type, user_id, created_by)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE is_active = 1`,
|
||||
[notificationType, userId, createdBy]
|
||||
);
|
||||
return result.insertId || result.affectedRows > 0;
|
||||
},
|
||||
|
||||
// 수신자 제거 (soft delete)
|
||||
async remove(notificationType, userId) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE notification_recipients SET is_active = 0
|
||||
WHERE notification_type = ? AND user_id = ?`,
|
||||
[notificationType, userId]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
},
|
||||
|
||||
// 수신자 완전 삭제
|
||||
async delete(notificationType, userId) {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM notification_recipients
|
||||
WHERE notification_type = ? AND user_id = ?`,
|
||||
[notificationType, userId]
|
||||
);
|
||||
return result.affectedRows > 0;
|
||||
},
|
||||
|
||||
// 유형별 수신자 user_id 목록만 가져오기 (알림 생성용)
|
||||
async getRecipientIds(notificationType) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT user_id FROM notification_recipients
|
||||
WHERE notification_type = ? AND is_active = 1`,
|
||||
[notificationType]
|
||||
);
|
||||
return rows.map(r => r.user_id);
|
||||
},
|
||||
|
||||
// 사용자가 특정 유형의 수신자인지 확인
|
||||
async isRecipient(notificationType, userId) {
|
||||
const db = await getDb();
|
||||
const [[row]] = await db.query(
|
||||
`SELECT 1 FROM notification_recipients
|
||||
WHERE notification_type = ? AND user_id = ? AND is_active = 1`,
|
||||
[notificationType, userId]
|
||||
);
|
||||
return !!row;
|
||||
},
|
||||
|
||||
// 유형별 수신자 일괄 설정
|
||||
async setRecipients(notificationType, userIds, createdBy = null) {
|
||||
const db = await getDb();
|
||||
|
||||
// 기존 수신자 비활성화
|
||||
await db.query(
|
||||
`UPDATE notification_recipients SET is_active = 0
|
||||
WHERE notification_type = ?`,
|
||||
[notificationType]
|
||||
);
|
||||
|
||||
// 새 수신자 추가
|
||||
if (userIds && userIds.length > 0) {
|
||||
const values = userIds.map(userId => [notificationType, userId, createdBy]);
|
||||
await db.query(
|
||||
`INSERT INTO notification_recipients (notification_type, user_id, created_by)
|
||||
VALUES ?
|
||||
ON DUPLICATE KEY UPDATE is_active = 1`,
|
||||
[values]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = notificationRecipientModel;
|
||||
160
deploy/tkfb-package/api.hyungi.net/models/pageAccessModel.js
Normal file
160
deploy/tkfb-package/api.hyungi.net/models/pageAccessModel.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// models/pageAccessModel.js
|
||||
const db = require('../db/connection');
|
||||
|
||||
const PageAccessModel = {
|
||||
// 사용자의 페이지 권한 조회
|
||||
getUserPageAccess: (userId, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
p.id,
|
||||
p.page_key,
|
||||
p.page_name,
|
||||
p.page_path,
|
||||
p.category,
|
||||
p.is_admin_only,
|
||||
COALESCE(upa.can_access, 0) as can_access,
|
||||
upa.granted_at,
|
||||
upa.granted_by,
|
||||
granter.username as granted_by_username
|
||||
FROM pages p
|
||||
LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
|
||||
LEFT JOIN users granter ON upa.granted_by = granter.user_id
|
||||
WHERE p.is_admin_only = 0
|
||||
ORDER BY p.category, p.display_order
|
||||
`;
|
||||
|
||||
db.query(sql, [userId], callback);
|
||||
},
|
||||
|
||||
// 모든 페이지 목록 조회
|
||||
getAllPages: (callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
id,
|
||||
page_key,
|
||||
page_name,
|
||||
page_path,
|
||||
category,
|
||||
description,
|
||||
is_admin_only,
|
||||
display_order
|
||||
FROM pages
|
||||
WHERE is_admin_only = 0
|
||||
ORDER BY category, display_order
|
||||
`;
|
||||
|
||||
db.query(sql, callback);
|
||||
},
|
||||
|
||||
// 페이지 권한 부여
|
||||
grantPageAccess: (userId, pageId, grantedBy, callback) => {
|
||||
const sql = `
|
||||
INSERT INTO user_page_access (user_id, page_id, can_access, granted_by, granted_at)
|
||||
VALUES (?, ?, 1, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
can_access = 1,
|
||||
granted_by = ?,
|
||||
granted_at = NOW()
|
||||
`;
|
||||
|
||||
db.query(sql, [userId, pageId, grantedBy, grantedBy], callback);
|
||||
},
|
||||
|
||||
// 페이지 권한 회수
|
||||
revokePageAccess: (userId, pageId, callback) => {
|
||||
const sql = `
|
||||
DELETE FROM user_page_access
|
||||
WHERE user_id = ? AND page_id = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [userId, pageId], callback);
|
||||
},
|
||||
|
||||
// 여러 페이지 권한 일괄 설정
|
||||
setUserPageAccess: (userId, pageIds, grantedBy, callback) => {
|
||||
db.beginTransaction((err) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// 기존 권한 모두 삭제
|
||||
const deleteSql = 'DELETE FROM user_page_access WHERE user_id = ?';
|
||||
|
||||
db.query(deleteSql, [userId], (err) => {
|
||||
if (err) {
|
||||
return db.rollback(() => callback(err));
|
||||
}
|
||||
|
||||
// 새 권한이 없으면 커밋하고 종료
|
||||
if (!pageIds || pageIds.length === 0) {
|
||||
return db.commit((err) => {
|
||||
if (err) return db.rollback(() => callback(err));
|
||||
callback(null, { affectedRows: 0 });
|
||||
});
|
||||
}
|
||||
|
||||
// 새 권한 추가
|
||||
const values = pageIds.map(pageId => [userId, pageId, 1, grantedBy]);
|
||||
const insertSql = `
|
||||
INSERT INTO user_page_access (user_id, page_id, can_access, granted_by, granted_at)
|
||||
VALUES ?
|
||||
`;
|
||||
|
||||
db.query(insertSql, [values], (err, result) => {
|
||||
if (err) {
|
||||
return db.rollback(() => callback(err));
|
||||
}
|
||||
|
||||
db.commit((err) => {
|
||||
if (err) return db.rollback(() => callback(err));
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 특정 페이지 접근 권한 확인
|
||||
checkPageAccess: (userId, pageKey, callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
COALESCE(upa.can_access, 0) as can_access,
|
||||
p.is_admin_only
|
||||
FROM pages p
|
||||
LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
|
||||
WHERE p.page_key = ?
|
||||
`;
|
||||
|
||||
db.query(sql, [userId, pageKey], (err, results) => {
|
||||
if (err) return callback(err);
|
||||
if (results.length === 0) return callback(null, { can_access: false });
|
||||
callback(null, results[0]);
|
||||
});
|
||||
},
|
||||
|
||||
// 계정이 있는 작업자 목록 조회 (권한 관리용)
|
||||
getUsersWithAccounts: (callback) => {
|
||||
const sql = `
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.username,
|
||||
u.name,
|
||||
u.role_id,
|
||||
r.name as role_name,
|
||||
u.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
COUNT(upa.page_id) as granted_pages_count
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
LEFT JOIN workers w ON u.worker_id = w.worker_id
|
||||
LEFT JOIN user_page_access upa ON u.user_id = upa.user_id AND upa.can_access = 1
|
||||
WHERE u.is_active = 1
|
||||
AND u.role_id IN (4, 5)
|
||||
GROUP BY u.user_id
|
||||
ORDER BY w.worker_name, u.username
|
||||
`;
|
||||
|
||||
db.query(sql, callback);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PageAccessModel;
|
||||
358
deploy/tkfb-package/api.hyungi.net/models/patrolModel.js
Normal file
358
deploy/tkfb-package/api.hyungi.net/models/patrolModel.js
Normal file
@@ -0,0 +1,358 @@
|
||||
// patrolModel.js
|
||||
// 일일순회점검 시스템 모델
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const PatrolModel = {
|
||||
// ==================== 순회점검 세션 ====================
|
||||
|
||||
// 세션 생성 또는 조회
|
||||
getOrCreateSession: async (patrolDate, patrolTime, categoryId, inspectorId) => {
|
||||
const db = await getDb();
|
||||
|
||||
// 기존 세션 확인
|
||||
const [existingRows] = await db.query(`
|
||||
SELECT session_id, status, started_at, completed_at
|
||||
FROM daily_patrol_sessions
|
||||
WHERE patrol_date = ? AND patrol_time = ? AND category_id = ?
|
||||
`, [patrolDate, patrolTime, categoryId]);
|
||||
|
||||
if (existingRows.length > 0) {
|
||||
return existingRows[0];
|
||||
}
|
||||
|
||||
// 새 세션 생성
|
||||
const [result] = await db.query(`
|
||||
INSERT INTO daily_patrol_sessions (patrol_date, patrol_time, category_id, inspector_id, started_at)
|
||||
VALUES (?, ?, ?, ?, CURTIME())
|
||||
`, [patrolDate, patrolTime, categoryId, inspectorId]);
|
||||
|
||||
return {
|
||||
session_id: result.insertId,
|
||||
status: 'in_progress',
|
||||
started_at: new Date().toTimeString().slice(0, 8)
|
||||
};
|
||||
},
|
||||
|
||||
// 세션 조회
|
||||
getSession: async (sessionId) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT s.*, u.name AS inspector_name, wc.category_name
|
||||
FROM daily_patrol_sessions s
|
||||
LEFT JOIN users u ON s.inspector_id = u.user_id
|
||||
LEFT JOIN workplace_categories wc ON s.category_id = wc.category_id
|
||||
WHERE s.session_id = ?
|
||||
`, [sessionId]);
|
||||
return rows[0] || null;
|
||||
},
|
||||
|
||||
// 세션 목록 조회
|
||||
getSessions: async (filters = {}) => {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT s.*, u.name AS inspector_name, wc.category_name,
|
||||
(SELECT COUNT(*) FROM patrol_check_records WHERE session_id = s.session_id AND is_checked = 1) AS checked_count,
|
||||
(SELECT COUNT(*) FROM patrol_check_records WHERE session_id = s.session_id) AS total_count
|
||||
FROM daily_patrol_sessions s
|
||||
LEFT JOIN users u ON s.inspector_id = u.user_id
|
||||
LEFT JOIN workplace_categories wc ON s.category_id = wc.category_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
if (filters.patrol_date) {
|
||||
query += ' AND s.patrol_date = ?';
|
||||
params.push(filters.patrol_date);
|
||||
}
|
||||
if (filters.patrol_time) {
|
||||
query += ' AND s.patrol_time = ?';
|
||||
params.push(filters.patrol_time);
|
||||
}
|
||||
if (filters.category_id) {
|
||||
query += ' AND s.category_id = ?';
|
||||
params.push(filters.category_id);
|
||||
}
|
||||
if (filters.status) {
|
||||
query += ' AND s.status = ?';
|
||||
params.push(filters.status);
|
||||
}
|
||||
|
||||
query += ' ORDER BY s.patrol_date DESC, s.patrol_time DESC';
|
||||
|
||||
if (filters.limit) {
|
||||
query += ' LIMIT ?';
|
||||
params.push(parseInt(filters.limit));
|
||||
}
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 세션 완료 처리
|
||||
completeSession: async (sessionId) => {
|
||||
const db = await getDb();
|
||||
await db.query(`
|
||||
UPDATE daily_patrol_sessions
|
||||
SET status = 'completed', completed_at = CURTIME(), updated_at = NOW()
|
||||
WHERE session_id = ?
|
||||
`, [sessionId]);
|
||||
return true;
|
||||
},
|
||||
|
||||
// 세션 메모 업데이트
|
||||
updateSessionNotes: async (sessionId, notes) => {
|
||||
const db = await getDb();
|
||||
await db.query(`
|
||||
UPDATE daily_patrol_sessions
|
||||
SET notes = ?, updated_at = NOW()
|
||||
WHERE session_id = ?
|
||||
`, [notes, sessionId]);
|
||||
return true;
|
||||
},
|
||||
|
||||
// ==================== 체크리스트 항목 ====================
|
||||
|
||||
// 체크리스트 항목 조회 (공장/작업장별 필터링)
|
||||
getChecklistItems: async (categoryId = null, workplaceId = null) => {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT *
|
||||
FROM patrol_checklist_items
|
||||
WHERE is_active = 1
|
||||
AND (workplace_id IS NULL OR workplace_id = ?)
|
||||
AND (category_id IS NULL OR category_id = ?)
|
||||
ORDER BY check_category, display_order, check_item
|
||||
`;
|
||||
const [rows] = await db.query(query, [workplaceId, categoryId]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 체크리스트 항목 CRUD
|
||||
createChecklistItem: async (data) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`
|
||||
INSERT INTO patrol_checklist_items (workplace_id, category_id, check_category, check_item, description, display_order, is_required)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, [data.workplace_id, data.category_id, data.check_category, data.check_item, data.description, data.display_order || 0, data.is_required !== false]);
|
||||
return result.insertId;
|
||||
},
|
||||
|
||||
updateChecklistItem: async (itemId, data) => {
|
||||
const db = await getDb();
|
||||
const fields = [];
|
||||
const params = [];
|
||||
|
||||
['workplace_id', 'category_id', 'check_category', 'check_item', 'description', 'display_order', 'is_required', 'is_active'].forEach(key => {
|
||||
if (data[key] !== undefined) {
|
||||
fields.push(`${key} = ?`);
|
||||
params.push(data[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (fields.length === 0) return false;
|
||||
|
||||
params.push(itemId);
|
||||
await db.query(`UPDATE patrol_checklist_items SET ${fields.join(', ')}, updated_at = NOW() WHERE item_id = ?`, params);
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteChecklistItem: async (itemId) => {
|
||||
const db = await getDb();
|
||||
await db.query('UPDATE patrol_checklist_items SET is_active = 0, updated_at = NOW() WHERE item_id = ?', [itemId]);
|
||||
return true;
|
||||
},
|
||||
|
||||
// ==================== 체크 기록 ====================
|
||||
|
||||
// 작업장별 체크 기록 조회
|
||||
getCheckRecords: async (sessionId, workplaceId = null) => {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT r.*, ci.check_category, ci.check_item, ci.is_required
|
||||
FROM patrol_check_records r
|
||||
JOIN patrol_checklist_items ci ON r.check_item_id = ci.item_id
|
||||
WHERE r.session_id = ?
|
||||
`;
|
||||
const params = [sessionId];
|
||||
|
||||
if (workplaceId) {
|
||||
query += ' AND r.workplace_id = ?';
|
||||
params.push(workplaceId);
|
||||
}
|
||||
|
||||
query += ' ORDER BY ci.check_category, ci.display_order';
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 체크 기록 저장 (upsert)
|
||||
saveCheckRecord: async (sessionId, workplaceId, checkItemId, isChecked, checkResult = null, note = null) => {
|
||||
const db = await getDb();
|
||||
await db.query(`
|
||||
INSERT INTO patrol_check_records (session_id, workplace_id, check_item_id, is_checked, check_result, note, checked_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_checked = VALUES(is_checked),
|
||||
check_result = VALUES(check_result),
|
||||
note = VALUES(note),
|
||||
checked_at = NOW()
|
||||
`, [sessionId, workplaceId, checkItemId, isChecked, checkResult, note]);
|
||||
return true;
|
||||
},
|
||||
|
||||
// 여러 체크 기록 일괄 저장
|
||||
saveCheckRecords: async (sessionId, workplaceId, records) => {
|
||||
const db = await getDb();
|
||||
for (const record of records) {
|
||||
await db.query(`
|
||||
INSERT INTO patrol_check_records (session_id, workplace_id, check_item_id, is_checked, check_result, note, checked_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_checked = VALUES(is_checked),
|
||||
check_result = VALUES(check_result),
|
||||
note = VALUES(note),
|
||||
checked_at = NOW()
|
||||
`, [sessionId, workplaceId, record.check_item_id, record.is_checked, record.check_result, record.note]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// ==================== 작업장 물품 현황 ====================
|
||||
|
||||
// 작업장 물품 조회
|
||||
getWorkplaceItems: async (workplaceId, activeOnly = true) => {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT wi.*, u.name AS created_by_name, it.type_name, it.icon, it.color
|
||||
FROM workplace_items wi
|
||||
LEFT JOIN users u ON wi.created_by = u.user_id
|
||||
LEFT JOIN item_types it ON wi.item_type = it.type_code
|
||||
WHERE wi.workplace_id = ?
|
||||
`;
|
||||
if (activeOnly) {
|
||||
query += ' AND wi.is_active = 1';
|
||||
}
|
||||
query += ' ORDER BY wi.created_at DESC';
|
||||
|
||||
const [rows] = await db.query(query, [workplaceId]);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 물품 추가
|
||||
createWorkplaceItem: async (data) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(`
|
||||
INSERT INTO workplace_items
|
||||
(workplace_id, patrol_session_id, project_id, item_type, item_name, quantity, x_percent, y_percent, width_percent, height_percent, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
data.workplace_id,
|
||||
data.patrol_session_id,
|
||||
data.project_id,
|
||||
data.item_type,
|
||||
data.item_name,
|
||||
data.quantity || 1,
|
||||
data.x_percent,
|
||||
data.y_percent,
|
||||
data.width_percent,
|
||||
data.height_percent,
|
||||
data.created_by
|
||||
]);
|
||||
return result.insertId;
|
||||
},
|
||||
|
||||
// 물품 수정
|
||||
updateWorkplaceItem: async (itemId, data, userId) => {
|
||||
const db = await getDb();
|
||||
const fields = [];
|
||||
const params = [];
|
||||
|
||||
['item_type', 'item_name', 'quantity', 'x_percent', 'y_percent', 'width_percent', 'height_percent', 'is_active', 'project_id'].forEach(key => {
|
||||
if (data[key] !== undefined) {
|
||||
fields.push(`${key} = ?`);
|
||||
params.push(data[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (fields.length === 0) return false;
|
||||
|
||||
fields.push('updated_by = ?', 'updated_at = NOW()');
|
||||
params.push(userId, itemId);
|
||||
|
||||
await db.query(`UPDATE workplace_items SET ${fields.join(', ')} WHERE item_id = ?`, params);
|
||||
return true;
|
||||
},
|
||||
|
||||
// 물품 삭제 (비활성화)
|
||||
deleteWorkplaceItem: async (itemId, userId) => {
|
||||
const db = await getDb();
|
||||
await db.query('UPDATE workplace_items SET is_active = 0, updated_by = ?, updated_at = NOW() WHERE item_id = ?', [userId, itemId]);
|
||||
return true;
|
||||
},
|
||||
|
||||
// 물품 영구 삭제
|
||||
hardDeleteWorkplaceItem: async (itemId) => {
|
||||
const db = await getDb();
|
||||
await db.query('DELETE FROM workplace_items WHERE item_id = ?', [itemId]);
|
||||
return true;
|
||||
},
|
||||
|
||||
// ==================== 물품 유형 ====================
|
||||
|
||||
// 물품 유형 목록 조회
|
||||
getItemTypes: async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT * FROM item_types WHERE is_active = 1 ORDER BY display_order');
|
||||
return rows;
|
||||
},
|
||||
|
||||
// ==================== 대시보드/통계 ====================
|
||||
|
||||
// 오늘 순회점검 현황
|
||||
getTodayPatrolStatus: async (categoryId = null) => {
|
||||
const db = await getDb();
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
let query = `
|
||||
SELECT s.session_id, s.patrol_time, s.status, s.inspector_id, u.name AS inspector_name,
|
||||
s.started_at, s.completed_at,
|
||||
(SELECT COUNT(*) FROM patrol_check_records WHERE session_id = s.session_id AND is_checked = 1) AS checked_count,
|
||||
(SELECT COUNT(*) FROM patrol_check_records WHERE session_id = s.session_id) AS total_count
|
||||
FROM daily_patrol_sessions s
|
||||
LEFT JOIN users u ON s.inspector_id = u.user_id
|
||||
WHERE s.patrol_date = ?
|
||||
`;
|
||||
const params = [today];
|
||||
|
||||
if (categoryId) {
|
||||
query += ' AND s.category_id = ?';
|
||||
params.push(categoryId);
|
||||
}
|
||||
|
||||
query += ' ORDER BY s.patrol_time';
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
return rows;
|
||||
},
|
||||
|
||||
// 작업장별 점검 현황 (세션 기준)
|
||||
getWorkplaceCheckStatus: async (sessionId) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT w.workplace_id, w.workplace_name,
|
||||
COUNT(DISTINCT r.check_item_id) AS checked_count,
|
||||
(SELECT COUNT(*) FROM patrol_checklist_items WHERE is_active = 1) AS total_items,
|
||||
MAX(r.checked_at) AS last_check_time
|
||||
FROM workplaces w
|
||||
LEFT JOIN patrol_check_records r ON w.workplace_id = r.workplace_id AND r.session_id = ?
|
||||
WHERE w.is_active = 1
|
||||
GROUP BY w.workplace_id
|
||||
ORDER BY w.workplace_name
|
||||
`, [sessionId]);
|
||||
return rows;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = PatrolModel;
|
||||
96
deploy/tkfb-package/api.hyungi.net/models/projectModel.js
Normal file
96
deploy/tkfb-package/api.hyungi.net/models/projectModel.js
Normal file
@@ -0,0 +1,96 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const create = async (project) => {
|
||||
const db = await getDb();
|
||||
const {
|
||||
job_no, project_name,
|
||||
contract_date, due_date,
|
||||
delivery_method, site, pm,
|
||||
is_active = true,
|
||||
project_status = 'active',
|
||||
completed_date = null
|
||||
} = project;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO projects
|
||||
(job_no, project_name, contract_date, due_date, delivery_method, site, pm, is_active, project_status, completed_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[job_no, project_name, contract_date, due_date, delivery_method, site, pm, is_active, project_status, completed_date]
|
||||
);
|
||||
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
const getAll = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT project_id, job_no, project_name, contract_date, due_date, delivery_method, site, pm, is_active, project_status, completed_date, created_at, updated_at FROM projects ORDER BY project_id DESC`
|
||||
);
|
||||
return rows;
|
||||
};
|
||||
|
||||
// 활성 프로젝트만 조회 (작업보고서용)
|
||||
const getActiveProjects = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT project_id, job_no, project_name, contract_date, due_date, delivery_method, site, pm, is_active, project_status, completed_date, created_at, updated_at FROM projects
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY project_name ASC`
|
||||
);
|
||||
return rows;
|
||||
};
|
||||
|
||||
const getById = async (project_id) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT project_id, job_no, project_name, contract_date, due_date, delivery_method, site, pm, is_active, project_status, completed_date, created_at, updated_at FROM projects WHERE project_id = ?`,
|
||||
[project_id]
|
||||
);
|
||||
return rows[0];
|
||||
};
|
||||
|
||||
const update = async (project) => {
|
||||
const db = await getDb();
|
||||
const {
|
||||
project_id, job_no, project_name,
|
||||
contract_date, due_date,
|
||||
delivery_method, site, pm,
|
||||
is_active, project_status, completed_date
|
||||
} = project;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE projects
|
||||
SET job_no = ?,
|
||||
project_name = ?,
|
||||
contract_date = ?,
|
||||
due_date = ?,
|
||||
delivery_method= ?,
|
||||
site = ?,
|
||||
pm = ?,
|
||||
is_active = ?,
|
||||
project_status = ?,
|
||||
completed_date = ?
|
||||
WHERE project_id = ?`,
|
||||
[job_no, project_name, contract_date, due_date, delivery_method, site, pm, is_active, project_status, completed_date, project_id]
|
||||
);
|
||||
|
||||
return result.affectedRows;
|
||||
};
|
||||
|
||||
const remove = async (project_id) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM projects WHERE project_id = ?`,
|
||||
[project_id]
|
||||
);
|
||||
return result.affectedRows;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
getActiveProjects,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
43
deploy/tkfb-package/api.hyungi.net/models/roleModel.js
Normal file
43
deploy/tkfb-package/api.hyungi.net/models/roleModel.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
class RoleModel {
|
||||
/**
|
||||
* 모든 역할 목록을 조회합니다.
|
||||
* @returns {Promise<Array>} 역할 목록
|
||||
*/
|
||||
static async findAll() {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT id, name, description FROM roles ORDER BY id');
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID로 특정 역할을 조회합니다.
|
||||
* @param {number} id - 역할 ID
|
||||
* @returns {Promise<Object>} 역할 객체
|
||||
*/
|
||||
static async findById(id) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT id, name, description FROM roles WHERE id = ?', [id]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할에 속한 모든 권한을 조회합니다.
|
||||
* @param {number} roleId - 역할 ID
|
||||
* @returns {Promise<Array>} 권한 이름 목록
|
||||
*/
|
||||
static async findPermissionsByRoleId(roleId) {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT p.name
|
||||
FROM permissions p
|
||||
JOIN role_permissions rp ON p.id = rp.permission_id
|
||||
WHERE rp.role_id = ?`,
|
||||
[roleId]
|
||||
);
|
||||
return rows.map(p => p.name);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RoleModel;
|
||||
142
deploy/tkfb-package/api.hyungi.net/models/taskModel.js
Normal file
142
deploy/tkfb-package/api.hyungi.net/models/taskModel.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 작업 모델
|
||||
*
|
||||
* @author TK-FB-Project
|
||||
* @since 2026-01-26
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// ==================== 작업 CRUD ====================
|
||||
|
||||
/**
|
||||
* 작업 생성
|
||||
*/
|
||||
const createTask = async (taskData) => {
|
||||
const db = await getDb();
|
||||
const { work_type_id, task_name, description } = taskData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO tasks (work_type_id, task_name, description, is_active)
|
||||
VALUES (?, ?, ?, 1)`,
|
||||
[work_type_id || null, task_name, description || null]
|
||||
);
|
||||
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
/**
|
||||
* 전체 작업 목록 조회 (공정 정보 포함)
|
||||
*/
|
||||
const getAllTasks = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.task_id, t.work_type_id, t.task_name, t.description, t.is_active,
|
||||
t.created_at, t.updated_at,
|
||||
wt.name as work_type_name, wt.category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
ORDER BY wt.category ASC, t.task_id DESC`
|
||||
);
|
||||
return rows;
|
||||
};
|
||||
|
||||
/**
|
||||
* 활성 작업만 조회
|
||||
*/
|
||||
const getActiveTasks = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.task_id, t.work_type_id, t.task_name, t.description,
|
||||
wt.name as work_type_name, wt.category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE t.is_active = 1
|
||||
ORDER BY wt.category ASC, t.task_name ASC`
|
||||
);
|
||||
return rows;
|
||||
};
|
||||
|
||||
/**
|
||||
* 공정별 작업 목록 조회 (활성 작업만)
|
||||
*/
|
||||
const getTasksByWorkType = async (workTypeId) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.task_id, t.work_type_id, t.task_name, t.description, t.is_active,
|
||||
t.created_at, t.updated_at,
|
||||
wt.name as work_type_name, wt.category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE t.work_type_id = ? AND t.is_active = 1
|
||||
ORDER BY t.task_name ASC`,
|
||||
[workTypeId]
|
||||
);
|
||||
return rows;
|
||||
};
|
||||
|
||||
/**
|
||||
* 단일 작업 조회
|
||||
*/
|
||||
const getTaskById = async (taskId) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.task_id, t.work_type_id, t.task_name, t.description, t.is_active,
|
||||
t.created_at, t.updated_at,
|
||||
wt.name as work_type_name, wt.category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE t.task_id = ?`,
|
||||
[taskId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업 수정
|
||||
*/
|
||||
const updateTask = async (taskId, taskData) => {
|
||||
const db = await getDb();
|
||||
const { work_type_id, task_name, description, is_active } = taskData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE tasks
|
||||
SET work_type_id = ?,
|
||||
task_name = ?,
|
||||
description = ?,
|
||||
is_active = ?,
|
||||
updated_at = NOW()
|
||||
WHERE task_id = ?`,
|
||||
[
|
||||
work_type_id || null,
|
||||
task_name,
|
||||
description || null,
|
||||
is_active !== undefined ? is_active : 1,
|
||||
taskId
|
||||
]
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업 삭제
|
||||
*/
|
||||
const deleteTask = async (taskId) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM tasks WHERE task_id = ?`,
|
||||
[taskId]
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createTask,
|
||||
getAllTasks,
|
||||
getActiveTasks,
|
||||
getTasksByWorkType,
|
||||
getTaskById,
|
||||
updateTask,
|
||||
deleteTask
|
||||
};
|
||||
994
deploy/tkfb-package/api.hyungi.net/models/tbmModel.js
Normal file
994
deploy/tkfb-package/api.hyungi.net/models/tbmModel.js
Normal file
@@ -0,0 +1,994 @@
|
||||
// models/tbmModel.js - TBM 시스템 모델
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const TbmModel = {
|
||||
// ==================== TBM 세션 관련 ====================
|
||||
|
||||
/**
|
||||
* TBM 세션 생성
|
||||
*/
|
||||
createSession: async (sessionData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
INSERT INTO tbm_sessions
|
||||
(session_date, leader_id, project_id, work_type_id, task_id, work_location, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
sessionData.session_date,
|
||||
sessionData.leader_id,
|
||||
sessionData.project_id || null,
|
||||
sessionData.work_type_id || null,
|
||||
sessionData.task_id || null,
|
||||
sessionData.work_location || null,
|
||||
sessionData.created_by
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 날짜의 TBM 세션 조회
|
||||
*/
|
||||
getSessionsByDate: async (date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
s.*,
|
||||
w.worker_name as leader_name,
|
||||
w.job_type as leader_job_type,
|
||||
u.username as created_by_username,
|
||||
u.name as created_by_name,
|
||||
COUNT(DISTINCT ta.worker_id) as team_member_count,
|
||||
-- 첫 번째 팀원의 작업 정보 가져오기
|
||||
first_ta.project_id,
|
||||
first_ta.work_type_id,
|
||||
first_ta.task_id,
|
||||
first_ta.workplace_id,
|
||||
first_p.project_name,
|
||||
first_wt.name as work_type_name,
|
||||
first_t.task_name,
|
||||
first_wp.workplace_name as work_location
|
||||
FROM tbm_sessions s
|
||||
LEFT JOIN workers w ON s.leader_id = w.worker_id
|
||||
LEFT JOIN users u ON s.created_by = u.user_id
|
||||
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
|
||||
-- 첫 번째 팀원 정보 (가장 먼저 등록된 작업)
|
||||
LEFT JOIN (
|
||||
SELECT * FROM tbm_team_assignments
|
||||
WHERE (session_id, assignment_id) IN (
|
||||
SELECT session_id, MIN(assignment_id)
|
||||
FROM tbm_team_assignments
|
||||
GROUP BY session_id
|
||||
)
|
||||
) first_ta ON s.session_id = first_ta.session_id
|
||||
LEFT JOIN projects first_p ON first_ta.project_id = first_p.project_id
|
||||
LEFT JOIN work_types first_wt ON first_ta.work_type_id = first_wt.id
|
||||
LEFT JOIN tasks first_t ON first_ta.task_id = first_t.task_id
|
||||
LEFT JOIN workplaces first_wp ON first_ta.workplace_id = first_wp.workplace_id
|
||||
WHERE s.session_date = ?
|
||||
GROUP BY s.session_id
|
||||
ORDER BY s.session_id DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [date]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 상세 조회
|
||||
*/
|
||||
getSessionById: async (sessionId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
s.*,
|
||||
w.worker_name as leader_name,
|
||||
w.job_type as leader_job_type,
|
||||
w.phone_number as leader_phone,
|
||||
p.project_name,
|
||||
p.job_no,
|
||||
p.site,
|
||||
wt.name as work_type_name,
|
||||
wt.category as work_type_category,
|
||||
t.task_name,
|
||||
t.description as task_description,
|
||||
u.username as created_by_username,
|
||||
u.name as created_by_name
|
||||
FROM tbm_sessions s
|
||||
LEFT JOIN workers w ON s.leader_id = w.worker_id
|
||||
LEFT JOIN projects p ON s.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON s.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON s.task_id = t.task_id
|
||||
LEFT JOIN users u ON s.created_by = u.user_id
|
||||
WHERE s.session_id = ?
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [sessionId]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 수정
|
||||
*/
|
||||
updateSession: async (sessionId, sessionData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
UPDATE tbm_sessions
|
||||
SET
|
||||
project_id = ?,
|
||||
work_location = ?,
|
||||
status = ?,
|
||||
updated_at = NOW()
|
||||
WHERE session_id = ?
|
||||
`;
|
||||
|
||||
const values = [
|
||||
sessionData.project_id,
|
||||
sessionData.work_location,
|
||||
sessionData.status,
|
||||
sessionId
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션 완료 처리
|
||||
*/
|
||||
completeSession: async (sessionId, endTime, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
UPDATE tbm_sessions
|
||||
SET
|
||||
status = 'completed',
|
||||
end_time = ?,
|
||||
updated_at = NOW()
|
||||
WHERE session_id = ?
|
||||
`;
|
||||
|
||||
const [result] = await db.query(sql, [endTime, sessionId]);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 팀 구성 관련 ====================
|
||||
|
||||
/**
|
||||
* 팀원 추가 (작업자별 상세 정보 포함)
|
||||
*/
|
||||
addTeamMember: async (assignmentData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
INSERT INTO tbm_team_assignments
|
||||
(session_id, worker_id, assigned_role, work_detail, is_present, absence_reason,
|
||||
project_id, work_type_id, task_id, workplace_category_id, workplace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
assigned_role = VALUES(assigned_role),
|
||||
work_detail = VALUES(work_detail),
|
||||
is_present = VALUES(is_present),
|
||||
absence_reason = VALUES(absence_reason),
|
||||
project_id = VALUES(project_id),
|
||||
work_type_id = VALUES(work_type_id),
|
||||
task_id = VALUES(task_id),
|
||||
workplace_category_id = VALUES(workplace_category_id),
|
||||
workplace_id = VALUES(workplace_id)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
assignmentData.session_id,
|
||||
assignmentData.worker_id,
|
||||
assignmentData.assigned_role,
|
||||
assignmentData.work_detail,
|
||||
assignmentData.is_present !== undefined ? assignmentData.is_present : true,
|
||||
assignmentData.absence_reason,
|
||||
assignmentData.project_id || null,
|
||||
assignmentData.work_type_id || null,
|
||||
assignmentData.task_id || null,
|
||||
assignmentData.workplace_category_id || null,
|
||||
assignmentData.workplace_id || null
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 팀 구성 일괄 추가 (작업자별 상세 정보 포함)
|
||||
*/
|
||||
addTeamMembers: async (sessionId, members, callback) => {
|
||||
try {
|
||||
if (!members || members.length === 0) {
|
||||
return callback(null, { affectedRows: 0 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const values = members.map(m => [
|
||||
sessionId,
|
||||
m.worker_id,
|
||||
m.assigned_role || null,
|
||||
m.work_detail || null,
|
||||
m.is_present !== undefined ? m.is_present : true,
|
||||
m.absence_reason || null,
|
||||
m.project_id || null,
|
||||
m.work_type_id || null,
|
||||
m.task_id || null,
|
||||
m.workplace_category_id || null,
|
||||
m.workplace_id || null
|
||||
]);
|
||||
|
||||
const sql = `
|
||||
INSERT INTO tbm_team_assignments
|
||||
(session_id, worker_id, assigned_role, work_detail, is_present, absence_reason,
|
||||
project_id, work_type_id, task_id, workplace_category_id, workplace_id)
|
||||
VALUES ?
|
||||
`;
|
||||
|
||||
const [result] = await db.query(sql, [values]);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션의 팀 구성 조회 (작업자별 상세 정보 포함)
|
||||
*/
|
||||
getTeamMembers: async (sessionId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
ta.*,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
w.phone_number,
|
||||
w.department,
|
||||
p.project_name,
|
||||
wt.name as work_type_name,
|
||||
t.task_name,
|
||||
wc.category_name AS workplace_category_name,
|
||||
wp.workplace_name
|
||||
FROM tbm_team_assignments ta
|
||||
INNER JOIN workers w ON ta.worker_id = w.worker_id
|
||||
LEFT JOIN projects p ON ta.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON ta.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON ta.task_id = t.task_id
|
||||
LEFT JOIN workplace_categories wc ON ta.workplace_category_id = wc.category_id
|
||||
LEFT JOIN workplaces wp ON ta.workplace_id = wp.workplace_id
|
||||
WHERE ta.session_id = ?
|
||||
ORDER BY ta.assigned_at DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [sessionId]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 팀원 제거
|
||||
*/
|
||||
removeTeamMember: async (sessionId, workerId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
DELETE FROM tbm_team_assignments
|
||||
WHERE session_id = ? AND worker_id = ?
|
||||
`;
|
||||
|
||||
const [result] = await db.query(sql, [sessionId, workerId]);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 세션의 모든 팀원 삭제
|
||||
*/
|
||||
clearAllTeamMembers: async (sessionId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
DELETE FROM tbm_team_assignments
|
||||
WHERE session_id = ?
|
||||
`;
|
||||
|
||||
const [result] = await db.query(sql, [sessionId]);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 안전 체크리스트 관련 ====================
|
||||
|
||||
/**
|
||||
* 모든 안전 체크 항목 조회
|
||||
*/
|
||||
getAllSafetyChecks: async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM tbm_safety_checks
|
||||
WHERE is_active = 1
|
||||
ORDER BY check_category, display_order
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 카테고리별 안전 체크 항목 조회
|
||||
*/
|
||||
getSafetyChecksByCategory: async (category, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT *
|
||||
FROM tbm_safety_checks
|
||||
WHERE check_category = ? AND is_active = 1
|
||||
ORDER BY display_order
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [category]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션의 안전 체크 기록 조회
|
||||
*/
|
||||
getSafetyRecords: async (sessionId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
sr.*,
|
||||
sc.check_category,
|
||||
sc.check_item,
|
||||
sc.description,
|
||||
sc.is_required,
|
||||
u.username as checked_by_username,
|
||||
u.name as checked_by_name
|
||||
FROM tbm_safety_records sr
|
||||
INNER JOIN tbm_safety_checks sc ON sr.check_id = sc.check_id
|
||||
LEFT JOIN users u ON sr.checked_by = u.user_id
|
||||
WHERE sr.session_id = ?
|
||||
ORDER BY sc.check_category, sc.display_order
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [sessionId]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 기록 저장/업데이트
|
||||
*/
|
||||
saveSafetyRecord: async (recordData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
INSERT INTO tbm_safety_records
|
||||
(session_id, check_id, is_checked, notes, checked_by, checked_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_checked = VALUES(is_checked),
|
||||
notes = VALUES(notes),
|
||||
checked_by = VALUES(checked_by),
|
||||
checked_at = NOW()
|
||||
`;
|
||||
|
||||
const values = [
|
||||
recordData.session_id,
|
||||
recordData.check_id,
|
||||
recordData.is_checked,
|
||||
recordData.notes,
|
||||
recordData.checked_by
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 일괄 저장
|
||||
*/
|
||||
saveSafetyRecords: async (sessionId, records, checkedBy, callback) => {
|
||||
try {
|
||||
if (!records || records.length === 0) {
|
||||
return callback(null, { affectedRows: 0 });
|
||||
}
|
||||
|
||||
const db = await getDb();
|
||||
const values = records.map(r => [
|
||||
sessionId,
|
||||
r.check_id,
|
||||
r.is_checked,
|
||||
r.notes || null,
|
||||
checkedBy
|
||||
]);
|
||||
|
||||
const sql = `
|
||||
INSERT INTO tbm_safety_records
|
||||
(session_id, check_id, is_checked, notes, checked_by, checked_at)
|
||||
VALUES ?
|
||||
ON DUPLICATE KEY UPDATE
|
||||
is_checked = VALUES(is_checked),
|
||||
notes = VALUES(notes),
|
||||
checked_by = VALUES(checked_by),
|
||||
checked_at = NOW()
|
||||
`;
|
||||
|
||||
const [result] = await db.query(sql, [values]);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 작업 인계 관련 ====================
|
||||
|
||||
/**
|
||||
* 작업 인계 생성
|
||||
*/
|
||||
createHandover: async (handoverData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
INSERT INTO team_handovers
|
||||
(session_id, from_leader_id, to_leader_id, handover_date, handover_time,
|
||||
reason, handover_notes, worker_ids)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
handoverData.session_id,
|
||||
handoverData.from_leader_id,
|
||||
handoverData.to_leader_id,
|
||||
handoverData.handover_date,
|
||||
handoverData.handover_time,
|
||||
handoverData.reason,
|
||||
handoverData.handover_notes,
|
||||
JSON.stringify(handoverData.worker_ids || [])
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업 인계 확인
|
||||
*/
|
||||
confirmHandover: async (handoverId, confirmedBy, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
UPDATE team_handovers
|
||||
SET
|
||||
is_confirmed = 1,
|
||||
confirmed_at = NOW(),
|
||||
confirmed_by = ?
|
||||
WHERE handover_id = ?
|
||||
`;
|
||||
|
||||
const [result] = await db.query(sql, [confirmedBy, handoverId]);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 날짜의 작업 인계 목록 조회
|
||||
*/
|
||||
getHandoversByDate: async (date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
h.*,
|
||||
w1.worker_name as from_leader_name,
|
||||
w2.worker_name as to_leader_name,
|
||||
u.username as confirmed_by_username,
|
||||
u.name as confirmed_by_name
|
||||
FROM team_handovers h
|
||||
INNER JOIN workers w1 ON h.from_leader_id = w1.worker_id
|
||||
INNER JOIN workers w2 ON h.to_leader_id = w2.worker_id
|
||||
LEFT JOIN users u ON h.confirmed_by = u.user_id
|
||||
WHERE h.handover_date = ?
|
||||
ORDER BY h.handover_time DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [date]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 인수자가 받은 미확인 인계 건 조회
|
||||
*/
|
||||
getPendingHandovers: async (toLeaderId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
h.*,
|
||||
w1.worker_name as from_leader_name,
|
||||
w1.phone_number as from_leader_phone,
|
||||
s.work_location
|
||||
FROM team_handovers h
|
||||
INNER JOIN workers w1 ON h.from_leader_id = w1.worker_id
|
||||
LEFT JOIN tbm_sessions s ON h.session_id = s.session_id
|
||||
WHERE h.to_leader_id = ? AND h.is_confirmed = 0
|
||||
ORDER BY h.handover_date DESC, h.handover_time DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [toLeaderId]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 통계 및 리포트 ====================
|
||||
|
||||
/**
|
||||
* 특정 기간의 TBM 통계
|
||||
*/
|
||||
getTbmStatistics: async (startDate, endDate, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
DATE(session_date) as date,
|
||||
COUNT(DISTINCT session_id) as session_count,
|
||||
COUNT(DISTINCT leader_id) as leader_count,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count
|
||||
FROM tbm_sessions
|
||||
WHERE session_date BETWEEN ? AND ?
|
||||
GROUP BY DATE(session_date)
|
||||
ORDER BY date DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [startDate, endDate]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 리더별 TBM 진행 현황
|
||||
*/
|
||||
getLeaderStatistics: async (startDate, endDate, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
s.leader_id,
|
||||
w.worker_name as leader_name,
|
||||
COUNT(DISTINCT s.session_id) as total_sessions,
|
||||
SUM(CASE WHEN s.status = 'completed' THEN 1 ELSE 0 END) as completed_sessions,
|
||||
COUNT(DISTINCT ta.worker_id) as total_team_members
|
||||
FROM tbm_sessions s
|
||||
INNER JOIN workers w ON s.leader_id = w.worker_id
|
||||
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
|
||||
WHERE s.session_date BETWEEN ? AND ?
|
||||
GROUP BY s.leader_id
|
||||
ORDER BY total_sessions DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, [startDate, endDate]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업보고서가 작성되지 않은 TBM 세션의 팀 배정 조회
|
||||
* @param {number|null} userId - 조회할 사용자 ID (null이면 모든 TBM 조회 - 관리자용)
|
||||
*/
|
||||
getIncompleteWorkReports: async (userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// WHERE 조건 동적 생성
|
||||
let whereClause = `
|
||||
WHERE dwr.id IS NULL
|
||||
AND s.status = 'draft'
|
||||
`;
|
||||
const params = [];
|
||||
|
||||
// userId가 있으면 created_by 조건 추가 (일반 사용자)
|
||||
if (userId !== null && userId !== undefined) {
|
||||
whereClause = `
|
||||
WHERE s.created_by = ?
|
||||
AND dwr.id IS NULL
|
||||
AND s.status = 'draft'
|
||||
`;
|
||||
params.push(userId);
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
ta.assignment_id,
|
||||
ta.session_id,
|
||||
ta.worker_id,
|
||||
ta.project_id,
|
||||
ta.work_type_id,
|
||||
ta.task_id,
|
||||
ta.workplace_category_id,
|
||||
ta.workplace_id,
|
||||
s.session_date,
|
||||
s.status as session_status,
|
||||
s.created_by,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
p.project_name,
|
||||
wt.name as work_type_name,
|
||||
t.task_name,
|
||||
wp.workplace_name,
|
||||
wc.category_name,
|
||||
creator.name as created_by_name
|
||||
FROM tbm_team_assignments ta
|
||||
INNER JOIN tbm_sessions s ON ta.session_id = s.session_id
|
||||
INNER JOIN workers w ON ta.worker_id = w.worker_id
|
||||
LEFT JOIN users creator ON s.created_by = creator.user_id
|
||||
LEFT JOIN projects p ON ta.project_id = p.project_id
|
||||
LEFT JOIN work_types wt ON ta.work_type_id = wt.id
|
||||
LEFT JOIN tasks t ON ta.task_id = t.task_id
|
||||
LEFT JOIN workplaces wp ON ta.workplace_id = wp.workplace_id
|
||||
LEFT JOIN workplace_categories wc ON ta.workplace_category_id = wc.category_id
|
||||
LEFT JOIN daily_work_reports dwr ON ta.assignment_id = dwr.tbm_assignment_id
|
||||
${whereClause}
|
||||
ORDER BY s.session_date DESC, ta.assignment_id ASC
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
// ========== 안전 체크리스트 확장 메서드 ==========
|
||||
|
||||
/**
|
||||
* 유형별 안전 체크 항목 조회
|
||||
* @param {string} checkType - 체크 유형 (basic, weather, task)
|
||||
* @param {Object} options - 추가 옵션 (weatherCondition, taskId)
|
||||
*/
|
||||
getSafetyChecksByType: async (checkType, options = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let sql = `
|
||||
SELECT sc.*,
|
||||
wc.condition_name as weather_condition_name,
|
||||
wc.icon as weather_icon,
|
||||
t.task_name
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN weather_conditions wc ON sc.weather_condition = wc.condition_code
|
||||
LEFT JOIN tasks t ON sc.task_id = t.task_id
|
||||
WHERE sc.is_active = 1 AND sc.check_type = ?
|
||||
`;
|
||||
const params = [checkType];
|
||||
|
||||
if (checkType === 'weather' && options.weatherCondition) {
|
||||
sql += ' AND sc.weather_condition = ?';
|
||||
params.push(options.weatherCondition);
|
||||
}
|
||||
|
||||
if (checkType === 'task' && options.taskId) {
|
||||
sql += ' AND sc.task_id = ?';
|
||||
params.push(options.taskId);
|
||||
}
|
||||
|
||||
sql += ' ORDER BY sc.check_category, sc.display_order';
|
||||
|
||||
const [rows] = await db.query(sql, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 날씨 조건별 안전 체크 항목 조회 (복수 조건)
|
||||
* @param {string[]} conditions - 날씨 조건 배열 ['rain', 'wind']
|
||||
*/
|
||||
getSafetyChecksByWeather: async (conditions, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
if (!conditions || conditions.length === 0) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
const placeholders = conditions.map(() => '?').join(',');
|
||||
const sql = `
|
||||
SELECT sc.*,
|
||||
wc.condition_name as weather_condition_name,
|
||||
wc.icon as weather_icon
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN weather_conditions wc ON sc.weather_condition = wc.condition_code
|
||||
WHERE sc.is_active = 1
|
||||
AND sc.check_type = 'weather'
|
||||
AND sc.weather_condition IN (${placeholders})
|
||||
ORDER BY sc.weather_condition, sc.display_order
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, conditions);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업별 안전 체크 항목 조회 (복수 작업)
|
||||
* @param {number[]} taskIds - 작업 ID 배열
|
||||
*/
|
||||
getSafetyChecksByTasks: async (taskIds, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
if (!taskIds || taskIds.length === 0) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
const placeholders = taskIds.map(() => '?').join(',');
|
||||
const sql = `
|
||||
SELECT sc.*,
|
||||
t.task_name,
|
||||
wt.name as work_type_name
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN tasks t ON sc.task_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE sc.is_active = 1
|
||||
AND sc.check_type = 'task'
|
||||
AND sc.task_id IN (${placeholders})
|
||||
ORDER BY sc.task_id, sc.display_order
|
||||
`;
|
||||
|
||||
const [rows] = await db.query(sql, taskIds);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* TBM 세션에 맞는 필터링된 안전 체크 항목 조회
|
||||
* 기본 + 날씨 + 작업별 체크항목 통합 조회
|
||||
* @param {number} sessionId - TBM 세션 ID
|
||||
* @param {string[]} weatherConditions - 날씨 조건 배열 (optional)
|
||||
*/
|
||||
getFilteredSafetyChecks: async (sessionId, weatherConditions = [], callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 1. 세션 정보에서 작업 ID 목록 조회
|
||||
const [assignments] = await db.query(`
|
||||
SELECT DISTINCT task_id
|
||||
FROM tbm_team_assignments
|
||||
WHERE session_id = ? AND task_id IS NOT NULL
|
||||
`, [sessionId]);
|
||||
|
||||
const taskIds = assignments.map(a => a.task_id);
|
||||
|
||||
// 2. 기본 체크항목 조회
|
||||
const [basicChecks] = await db.query(`
|
||||
SELECT sc.*, 'basic' as section_type
|
||||
FROM tbm_safety_checks sc
|
||||
WHERE sc.is_active = 1 AND sc.check_type = 'basic'
|
||||
ORDER BY sc.check_category, sc.display_order
|
||||
`);
|
||||
|
||||
// 3. 날씨별 체크항목 조회
|
||||
let weatherChecks = [];
|
||||
if (weatherConditions && weatherConditions.length > 0) {
|
||||
const wcPlaceholders = weatherConditions.map(() => '?').join(',');
|
||||
const [rows] = await db.query(`
|
||||
SELECT sc.*, wc.condition_name as weather_condition_name, wc.icon as weather_icon,
|
||||
'weather' as section_type
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN weather_conditions wc ON sc.weather_condition = wc.condition_code
|
||||
WHERE sc.is_active = 1
|
||||
AND sc.check_type = 'weather'
|
||||
AND sc.weather_condition IN (${wcPlaceholders})
|
||||
ORDER BY sc.weather_condition, sc.display_order
|
||||
`, weatherConditions);
|
||||
weatherChecks = rows;
|
||||
}
|
||||
|
||||
// 4. 작업별 체크항목 조회
|
||||
let taskChecks = [];
|
||||
if (taskIds.length > 0) {
|
||||
const taskPlaceholders = taskIds.map(() => '?').join(',');
|
||||
const [rows] = await db.query(`
|
||||
SELECT sc.*, t.task_name, wt.name as work_type_name,
|
||||
'task' as section_type
|
||||
FROM tbm_safety_checks sc
|
||||
LEFT JOIN tasks t ON sc.task_id = t.task_id
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE sc.is_active = 1
|
||||
AND sc.check_type = 'task'
|
||||
AND sc.task_id IN (${taskPlaceholders})
|
||||
ORDER BY sc.task_id, sc.display_order
|
||||
`, taskIds);
|
||||
taskChecks = rows;
|
||||
}
|
||||
|
||||
// 5. 기존 체크 기록 조회
|
||||
const [existingRecords] = await db.query(`
|
||||
SELECT check_id, is_checked, notes
|
||||
FROM tbm_safety_records
|
||||
WHERE session_id = ?
|
||||
`, [sessionId]);
|
||||
|
||||
const recordMap = {};
|
||||
existingRecords.forEach(r => {
|
||||
recordMap[r.check_id] = { is_checked: r.is_checked, notes: r.notes };
|
||||
});
|
||||
|
||||
// 6. 기록과 병합
|
||||
const mergeWithRecords = (checks) => {
|
||||
return checks.map(check => ({
|
||||
...check,
|
||||
is_checked: recordMap[check.check_id]?.is_checked || false,
|
||||
notes: recordMap[check.check_id]?.notes || null
|
||||
}));
|
||||
};
|
||||
|
||||
const result = {
|
||||
basic: mergeWithRecords(basicChecks),
|
||||
weather: mergeWithRecords(weatherChecks),
|
||||
task: mergeWithRecords(taskChecks),
|
||||
totalCount: basicChecks.length + weatherChecks.length + taskChecks.length,
|
||||
weatherConditions: weatherConditions
|
||||
};
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 생성 (관리자용)
|
||||
*/
|
||||
createSafetyCheck: async (checkData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
INSERT INTO tbm_safety_checks
|
||||
(check_category, check_type, weather_condition, task_id, check_item, description, is_required, display_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const values = [
|
||||
checkData.check_category,
|
||||
checkData.check_type || 'basic',
|
||||
checkData.weather_condition || null,
|
||||
checkData.task_id || null,
|
||||
checkData.check_item,
|
||||
checkData.description || null,
|
||||
checkData.is_required !== false,
|
||||
checkData.display_order || 0
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, { insertId: result.insertId });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 수정 (관리자용)
|
||||
*/
|
||||
updateSafetyCheck: async (checkId, checkData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
UPDATE tbm_safety_checks
|
||||
SET check_category = ?,
|
||||
check_type = ?,
|
||||
weather_condition = ?,
|
||||
task_id = ?,
|
||||
check_item = ?,
|
||||
description = ?,
|
||||
is_required = ?,
|
||||
display_order = ?,
|
||||
is_active = ?,
|
||||
updated_at = NOW()
|
||||
WHERE check_id = ?
|
||||
`;
|
||||
|
||||
const values = [
|
||||
checkData.check_category,
|
||||
checkData.check_type || 'basic',
|
||||
checkData.weather_condition || null,
|
||||
checkData.task_id || null,
|
||||
checkData.check_item,
|
||||
checkData.description || null,
|
||||
checkData.is_required !== false,
|
||||
checkData.display_order || 0,
|
||||
checkData.is_active !== false,
|
||||
checkId
|
||||
];
|
||||
|
||||
const [result] = await db.query(sql, values);
|
||||
callback(null, { affectedRows: result.affectedRows });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 안전 체크 항목 삭제 (비활성화)
|
||||
*/
|
||||
deleteSafetyCheck: async (checkId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
// 실제 삭제 대신 비활성화
|
||||
const sql = `UPDATE tbm_safety_checks SET is_active = 0 WHERE check_id = ?`;
|
||||
|
||||
const [result] = await db.query(sql, [checkId]);
|
||||
callback(null, { affectedRows: result.affectedRows });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TbmModel;
|
||||
68
deploy/tkfb-package/api.hyungi.net/models/toolsModel.js
Normal file
68
deploy/tkfb-package/api.hyungi.net/models/toolsModel.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 1. 전체 도구 조회
|
||||
const getAll = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT id, name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note FROM Tools');
|
||||
return rows;
|
||||
};
|
||||
|
||||
// 2. 단일 도구 조회
|
||||
const getById = async (id) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query('SELECT id, name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note FROM Tools WHERE id = ?', [id]);
|
||||
return rows[0];
|
||||
};
|
||||
|
||||
// 3. 도구 생성
|
||||
const create = async (tool) => {
|
||||
const db = await getDb();
|
||||
const { name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note } = tool;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO Tools
|
||||
(name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note]
|
||||
);
|
||||
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
// 4. 도구 수정
|
||||
const update = async (id, tool) => {
|
||||
const db = await getDb();
|
||||
const { name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note } = tool;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE Tools
|
||||
SET name = ?,
|
||||
location = ?,
|
||||
stock = ?,
|
||||
status = ?,
|
||||
factory_id = ?,
|
||||
map_x = ?,
|
||||
map_y = ?,
|
||||
map_zone = ?,
|
||||
map_note = ?
|
||||
WHERE id = ?`,
|
||||
[name, location, stock, status, factory_id, map_x, map_y, map_zone, map_note, id]
|
||||
);
|
||||
|
||||
return result.affectedRows;
|
||||
};
|
||||
|
||||
// 5. 도구 삭제
|
||||
const remove = async (id) => {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query('DELETE FROM Tools WHERE id = ?', [id]);
|
||||
return result.affectedRows;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAll,
|
||||
getById,
|
||||
create,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
36
deploy/tkfb-package/api.hyungi.net/models/uploadModel.js
Normal file
36
deploy/tkfb-package/api.hyungi.net/models/uploadModel.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 1. 문서 업로드
|
||||
const create = async (doc) => {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
INSERT INTO uploaded_documents
|
||||
(title, tags, description, original_name, stored_name, file_path, file_type, file_size, submitted_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
const values = [
|
||||
doc.title,
|
||||
doc.tags,
|
||||
doc.description,
|
||||
doc.original_name,
|
||||
doc.stored_name,
|
||||
doc.file_path,
|
||||
doc.file_type,
|
||||
doc.file_size,
|
||||
doc.submitted_by
|
||||
];
|
||||
const [result] = await db.query(sql, values);
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
// 2. 전체 문서 목록 조회
|
||||
const getAll = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`SELECT * FROM uploaded_documents ORDER BY created_at DESC`);
|
||||
return rows;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll
|
||||
};
|
||||
82
deploy/tkfb-package/api.hyungi.net/models/userModel.js
Normal file
82
deploy/tkfb-package/api.hyungi.net/models/userModel.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 사용자 조회
|
||||
const findByUsername = async (username) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT u.user_id, u.username, u.password, u.name, u.email,
|
||||
u.role_id, r.name as role_name,
|
||||
u._access_level_old as access_level, u.worker_id, u.is_active,
|
||||
u.last_login_at, u.password_changed_at, u.failed_login_attempts,
|
||||
u.locked_until, u.created_at, u.updated_at
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.username = ?`, [username]
|
||||
);
|
||||
return rows[0];
|
||||
} catch (err) {
|
||||
console.error('DB 오류 - 사용자 조회 실패:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 로그인 실패 횟수를 1 증가시킵니다.
|
||||
* @param {number} userId - 사용자 ID
|
||||
*/
|
||||
const incrementFailedLoginAttempts = async (userId) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
await db.query(
|
||||
'UPDATE users SET failed_login_attempts = failed_login_attempts + 1 WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('DB 오류 - 로그인 실패 횟수 증가 실패:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 사용자의 계정을 잠급니다.
|
||||
* @param {number} userId - 사용자 ID
|
||||
*/
|
||||
const lockUserAccount = async (userId) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
await db.query(
|
||||
'UPDATE users SET locked_until = DATE_ADD(NOW(), INTERVAL 15 MINUTE) WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('DB 오류 - 계정 잠금 실패:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 로그인 성공 시, 마지막 로그인 시간을 업데이트하고 실패 횟수와 잠금 상태를 초기화합니다.
|
||||
* @param {number} userId - 사용자 ID
|
||||
*/
|
||||
const resetLoginAttempts = async (userId) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
await db.query(
|
||||
'UPDATE users SET last_login_at = NOW(), failed_login_attempts = 0, locked_until = NULL WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('DB 오류 - 로그인 상태 초기화 실패:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 명확한 내보내기
|
||||
module.exports = {
|
||||
findByUsername,
|
||||
incrementFailedLoginAttempts,
|
||||
lockUserAccount,
|
||||
resetLoginAttempts
|
||||
};
|
||||
@@ -0,0 +1,400 @@
|
||||
/**
|
||||
* vacationBalanceModel.js
|
||||
* 휴가 잔액 관련 데이터베이스 쿼리 모델
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const vacationBalanceModel = {
|
||||
/**
|
||||
* 특정 작업자의 모든 휴가 잔액 조회 (특정 연도)
|
||||
*/
|
||||
async getByWorkerAndYear(workerId, year, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.*,
|
||||
vt.type_name,
|
||||
vt.type_code,
|
||||
vt.priority,
|
||||
vt.is_special
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ? AND vbd.year = ?
|
||||
ORDER BY vt.priority ASC, vt.type_name ASC
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 작업자의 특정 휴가 유형 잔액 조회
|
||||
*/
|
||||
async getByWorkerTypeYear(workerId, vacationTypeId, year, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.*,
|
||||
vt.type_name,
|
||||
vt.type_code
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ?
|
||||
AND vbd.vacation_type_id = ?
|
||||
AND vbd.year = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId, vacationTypeId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 모든 작업자의 휴가 잔액 조회 (특정 연도)
|
||||
* - 연간 연차 현황 차트용
|
||||
*/
|
||||
async getAllByYear(year, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.*,
|
||||
w.worker_name,
|
||||
w.employment_status,
|
||||
vt.type_name,
|
||||
vt.type_code,
|
||||
vt.priority
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN workers w ON vbd.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.year = ?
|
||||
AND w.employment_status = 'employed'
|
||||
ORDER BY w.worker_name ASC, vt.priority ASC
|
||||
`;
|
||||
const [rows] = await db.query(query, [year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 생성
|
||||
*/
|
||||
async create(balanceData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `INSERT INTO vacation_balance_details SET ?`;
|
||||
const [rows] = await db.query(query, balanceData);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 수정
|
||||
*/
|
||||
async update(id, updateData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_balance_details SET ? WHERE id = ?`;
|
||||
const [rows] = await db.query(query, [updateData, id]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 삭제
|
||||
*/
|
||||
async delete(id, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `DELETE FROM vacation_balance_details WHERE id = ?`;
|
||||
const [rows] = await db.query(query, [id]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업자의 휴가 사용 일수 업데이트 (차감)
|
||||
* - 휴가 신청 승인 시 호출
|
||||
*/
|
||||
async deductDays(workerId, vacationTypeId, year, daysToDeduct, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
UPDATE vacation_balance_details
|
||||
SET used_days = used_days + ?,
|
||||
updated_at = NOW()
|
||||
WHERE worker_id = ?
|
||||
AND vacation_type_id = ?
|
||||
AND year = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [daysToDeduct, workerId, vacationTypeId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업자의 휴가 사용 일수 복구 (취소)
|
||||
* - 휴가 신청 취소/거부 시 호출
|
||||
*/
|
||||
async restoreDays(workerId, vacationTypeId, year, daysToRestore, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
UPDATE vacation_balance_details
|
||||
SET used_days = GREATEST(0, used_days - ?),
|
||||
updated_at = NOW()
|
||||
WHERE worker_id = ?
|
||||
AND vacation_type_id = ?
|
||||
AND year = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [daysToRestore, workerId, vacationTypeId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 작업자의 사용 가능한 휴가 일수 확인
|
||||
* - 우선순위가 높은 순서대로 차감 가능 여부 확인
|
||||
*/
|
||||
async getAvailableVacationDays(workerId, year, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.id,
|
||||
vbd.vacation_type_id,
|
||||
vt.type_name,
|
||||
vt.type_code,
|
||||
vt.priority,
|
||||
vbd.total_days,
|
||||
vbd.used_days,
|
||||
vbd.remaining_days
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ?
|
||||
AND vbd.year = ?
|
||||
AND vbd.remaining_days > 0
|
||||
ORDER BY vt.priority ASC
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업자별 휴가 잔액 일괄 생성 (연도별)
|
||||
* - 매년 초 또는 입사 시 사용
|
||||
*/
|
||||
async bulkCreate(balances, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
if (!balances || balances.length === 0) {
|
||||
return callback(new Error('생성할 휴가 잔액 데이터가 없습니다'));
|
||||
}
|
||||
|
||||
const query = `INSERT INTO vacation_balance_details
|
||||
(worker_id, vacation_type_id, year, total_days, used_days, notes, created_by)
|
||||
VALUES ?`;
|
||||
|
||||
const values = balances.map(b => [
|
||||
b.worker_id,
|
||||
b.vacation_type_id,
|
||||
b.year,
|
||||
b.total_days || 0,
|
||||
b.used_days || 0,
|
||||
b.notes || null,
|
||||
b.created_by
|
||||
]);
|
||||
|
||||
const [rows] = await db.query(query, [values]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 근속년수 기반 연차 일수 계산 (한국 근로기준법)
|
||||
* @param {Date} hireDate - 입사일
|
||||
* @param {number} targetYear - 대상 연도
|
||||
* @returns {number} - 부여받을 연차 일수
|
||||
*/
|
||||
calculateAnnualLeaveDays(hireDate, targetYear) {
|
||||
const hire = new Date(hireDate);
|
||||
const targetDate = new Date(targetYear, 0, 1);
|
||||
|
||||
// 근속 월수 계산
|
||||
const monthsDiff = (targetDate.getFullYear() - hire.getFullYear()) * 12
|
||||
+ (targetDate.getMonth() - hire.getMonth());
|
||||
|
||||
// 1년 미만: 월 1일
|
||||
if (monthsDiff < 12) {
|
||||
return Math.floor(monthsDiff);
|
||||
}
|
||||
|
||||
// 1년 이상: 15일 기본 + 2년마다 1일 추가 (최대 25일)
|
||||
const yearsWorked = Math.floor(monthsDiff / 12);
|
||||
const additionalDays = Math.floor((yearsWorked - 1) / 2);
|
||||
|
||||
return Math.min(15 + additionalDays, 25);
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 사용 시 우선순위에 따라 잔액에서 차감 (Promise 버전)
|
||||
* - 일일 근태 기록 저장 시 호출
|
||||
* @param {number} workerId - 작업자 ID
|
||||
* @param {number} year - 연도
|
||||
* @param {number} daysToDeduct - 차감할 일수 (1, 0.5, 0.25)
|
||||
* @returns {Promise<Object>} - 차감 결과
|
||||
*/
|
||||
async deductByPriority(workerId, year, daysToDeduct) {
|
||||
const db = await getDb();
|
||||
|
||||
// 우선순위순으로 잔여 일수가 있는 잔액 조회
|
||||
const [balances] = await db.query(`
|
||||
SELECT vbd.id, vbd.vacation_type_id, vbd.total_days, vbd.used_days,
|
||||
(vbd.total_days - vbd.used_days) as remaining_days,
|
||||
vt.type_code, vt.type_name, vt.priority
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ? AND vbd.year = ?
|
||||
AND (vbd.total_days - vbd.used_days) > 0
|
||||
ORDER BY vt.priority ASC
|
||||
`, [workerId, year]);
|
||||
|
||||
if (balances.length === 0) {
|
||||
// 잔액이 없어도 일단 기록은 저장 (경고만)
|
||||
console.warn(`[VacationBalance] 작업자 ${workerId}의 ${year}년 휴가 잔액이 없습니다`);
|
||||
return { success: false, message: '휴가 잔액이 없습니다', deducted: 0 };
|
||||
}
|
||||
|
||||
let remaining = daysToDeduct;
|
||||
const deductions = [];
|
||||
|
||||
for (const balance of balances) {
|
||||
if (remaining <= 0) break;
|
||||
|
||||
const available = parseFloat(balance.remaining_days);
|
||||
const toDeduct = Math.min(remaining, available);
|
||||
|
||||
if (toDeduct > 0) {
|
||||
await db.query(`
|
||||
UPDATE vacation_balance_details
|
||||
SET used_days = used_days + ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`, [toDeduct, balance.id]);
|
||||
|
||||
deductions.push({
|
||||
balance_id: balance.id,
|
||||
type_code: balance.type_code,
|
||||
type_name: balance.type_name,
|
||||
deducted: toDeduct
|
||||
});
|
||||
|
||||
remaining -= toDeduct;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[VacationBalance] 작업자 ${workerId}: ${daysToDeduct}일 차감 완료`, deductions);
|
||||
return { success: true, deductions, totalDeducted: daysToDeduct - remaining };
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 취소 시 우선순위 역순으로 복구 (Promise 버전)
|
||||
* @param {number} workerId - 작업자 ID
|
||||
* @param {number} year - 연도
|
||||
* @param {number} daysToRestore - 복구할 일수
|
||||
* @returns {Promise<Object>} - 복구 결과
|
||||
*/
|
||||
async restoreByPriority(workerId, year, daysToRestore) {
|
||||
const db = await getDb();
|
||||
|
||||
// 우선순위 역순으로 사용 일수가 있는 잔액 조회 (나중에 차감된 것부터 복구)
|
||||
const [balances] = await db.query(`
|
||||
SELECT vbd.id, vbd.vacation_type_id, vbd.used_days,
|
||||
vt.type_code, vt.type_name, vt.priority
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ? AND vbd.year = ?
|
||||
AND vbd.used_days > 0
|
||||
ORDER BY vt.priority DESC
|
||||
`, [workerId, year]);
|
||||
|
||||
let remaining = daysToRestore;
|
||||
const restorations = [];
|
||||
|
||||
for (const balance of balances) {
|
||||
if (remaining <= 0) break;
|
||||
|
||||
const usedDays = parseFloat(balance.used_days);
|
||||
const toRestore = Math.min(remaining, usedDays);
|
||||
|
||||
if (toRestore > 0) {
|
||||
await db.query(`
|
||||
UPDATE vacation_balance_details
|
||||
SET used_days = used_days - ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`, [toRestore, balance.id]);
|
||||
|
||||
restorations.push({
|
||||
balance_id: balance.id,
|
||||
type_code: balance.type_code,
|
||||
type_name: balance.type_name,
|
||||
restored: toRestore
|
||||
});
|
||||
|
||||
remaining -= toRestore;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[VacationBalance] 작업자 ${workerId}: ${daysToRestore}일 복구 완료`, restorations);
|
||||
return { success: true, restorations, totalRestored: daysToRestore - remaining };
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 ID로 휴가 잔액 조회
|
||||
*/
|
||||
async getById(id, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.*,
|
||||
w.worker_name,
|
||||
vt.type_name,
|
||||
vt.type_code
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN workers w ON vbd.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.id = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [id]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationBalanceModel;
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* vacationRequestModel.js
|
||||
* 휴가 신청 관련 데이터베이스 쿼리 모델
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const vacationRequestModel = {
|
||||
/**
|
||||
* 휴가 신청 생성
|
||||
*/
|
||||
async create(requestData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `INSERT INTO vacation_requests SET ?`;
|
||||
const [result] = await db.query(query, requestData);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 목록 조회 (필터링 지원)
|
||||
*/
|
||||
async getAll(filters = {}, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
vr.*,
|
||||
w.worker_name,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.deduct_days as vacation_deduct_days,
|
||||
requester.name as requester_name,
|
||||
reviewer.name as reviewer_name
|
||||
FROM vacation_requests vr
|
||||
INNER JOIN workers w ON vr.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
LEFT JOIN users requester ON vr.requested_by = requester.user_id
|
||||
LEFT JOIN users reviewer ON vr.reviewed_by = reviewer.user_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
// 작업자 필터
|
||||
if (filters.worker_id) {
|
||||
query += ` AND vr.worker_id = ?`;
|
||||
params.push(filters.worker_id);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (filters.status) {
|
||||
query += ` AND vr.status = ?`;
|
||||
params.push(filters.status);
|
||||
}
|
||||
|
||||
// 기간 필터
|
||||
if (filters.start_date) {
|
||||
query += ` AND vr.start_date >= ?`;
|
||||
params.push(filters.start_date);
|
||||
}
|
||||
|
||||
if (filters.end_date) {
|
||||
query += ` AND vr.end_date <= ?`;
|
||||
params.push(filters.end_date);
|
||||
}
|
||||
|
||||
// 휴가 유형 필터
|
||||
if (filters.vacation_type_id) {
|
||||
query += ` AND vr.vacation_type_id = ?`;
|
||||
params.push(filters.vacation_type_id);
|
||||
}
|
||||
|
||||
query += ` ORDER BY vr.created_at DESC`;
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 휴가 신청 조회
|
||||
*/
|
||||
async getById(requestId, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vr.*,
|
||||
w.worker_name,
|
||||
w.phone_number as worker_phone,
|
||||
w.email as worker_email,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code as vacation_type_code,
|
||||
vt.deduct_days as vacation_deduct_days,
|
||||
requester.name as requester_name,
|
||||
requester.username as requester_username,
|
||||
reviewer.name as reviewer_name,
|
||||
reviewer.username as reviewer_username
|
||||
FROM vacation_requests vr
|
||||
INNER JOIN workers w ON vr.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
LEFT JOIN users requester ON vr.requested_by = requester.user_id
|
||||
LEFT JOIN users reviewer ON vr.reviewed_by = reviewer.user_id
|
||||
WHERE vr.request_id = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [requestId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 수정
|
||||
*/
|
||||
async update(requestId, updateData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_requests SET ? WHERE request_id = ?`;
|
||||
const [result] = await db.query(query, [updateData, requestId]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 삭제
|
||||
*/
|
||||
async delete(requestId, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `DELETE FROM vacation_requests WHERE request_id = ?`;
|
||||
const [result] = await db.query(query, [requestId]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 승인/거부
|
||||
*/
|
||||
async updateStatus(requestId, statusData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
UPDATE vacation_requests
|
||||
SET
|
||||
status = ?,
|
||||
reviewed_by = ?,
|
||||
reviewed_at = NOW(),
|
||||
review_note = ?
|
||||
WHERE request_id = ?
|
||||
`;
|
||||
const [result] = await db.query(query, [
|
||||
statusData.status,
|
||||
statusData.reviewed_by,
|
||||
statusData.review_note || null,
|
||||
requestId
|
||||
]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 작업자의 대기 중인 휴가 신청 수
|
||||
*/
|
||||
async getPendingCount(workerId, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM vacation_requests
|
||||
WHERE worker_id = ? AND status = 'pending'
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 작업자의 승인된 휴가 일수 합계 (특정 기간)
|
||||
*/
|
||||
async getApprovedDaysInPeriod(workerId, startDate, endDate, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT COALESCE(SUM(days_used), 0) as total_days
|
||||
FROM vacation_requests
|
||||
WHERE worker_id = ?
|
||||
AND status = 'approved'
|
||||
AND start_date >= ?
|
||||
AND end_date <= ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId, startDate, endDate]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 기간 중복 체크
|
||||
*/
|
||||
async checkOverlap(workerId, startDate, endDate, excludeRequestId = null, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM vacation_requests
|
||||
WHERE worker_id = ?
|
||||
AND status IN ('pending', 'approved')
|
||||
AND (
|
||||
(start_date <= ? AND end_date >= ?) OR
|
||||
(start_date <= ? AND end_date >= ?) OR
|
||||
(start_date >= ? AND end_date <= ?)
|
||||
)
|
||||
`;
|
||||
const params = [workerId, startDate, startDate, endDate, endDate, startDate, endDate];
|
||||
|
||||
if (excludeRequestId) {
|
||||
query += ` AND request_id != ?`;
|
||||
params.push(excludeRequestId);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 모든 대기 중인 휴가 신청 (관리자용)
|
||||
*/
|
||||
async getAllPending(callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vr.*,
|
||||
w.worker_name,
|
||||
vt.type_name as vacation_type_name,
|
||||
requester.name as requester_name
|
||||
FROM vacation_requests vr
|
||||
INNER JOIN workers w ON vr.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
LEFT JOIN users requester ON vr.requested_by = requester.user_id
|
||||
WHERE vr.status = 'pending'
|
||||
ORDER BY vr.created_at ASC
|
||||
`;
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationRequestModel;
|
||||
132
deploy/tkfb-package/api.hyungi.net/models/vacationTypeModel.js
Normal file
132
deploy/tkfb-package/api.hyungi.net/models/vacationTypeModel.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* vacationTypeModel.js
|
||||
* 휴가 유형 관련 데이터베이스 쿼리 모델
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const vacationTypeModel = {
|
||||
/**
|
||||
* 모든 활성 휴가 유형 조회 (우선순위 순서대로)
|
||||
*/
|
||||
async getAll(callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM vacation_types
|
||||
WHERE is_active = 1
|
||||
ORDER BY priority ASC, id ASC
|
||||
`;
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 시스템 기본 휴가 유형만 조회
|
||||
*/
|
||||
async getSystemTypes(callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM vacation_types
|
||||
WHERE is_system = 1 AND is_active = 1
|
||||
ORDER BY priority ASC
|
||||
`;
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 ID로 휴가 유형 조회
|
||||
*/
|
||||
async getById(id, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `SELECT * FROM vacation_types WHERE id = ?`;
|
||||
const [rows] = await db.query(query, [id]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 코드로 조회
|
||||
*/
|
||||
async getByCode(code, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `SELECT * FROM vacation_types WHERE type_code = ?`;
|
||||
const [rows] = await db.query(query, [code]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 생성
|
||||
*/
|
||||
async create(typeData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `INSERT INTO vacation_types SET ?`;
|
||||
const [result] = await db.query(query, typeData);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 수정
|
||||
*/
|
||||
async update(id, updateData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_types SET ? WHERE id = ?`;
|
||||
const [result] = await db.query(query, [updateData, id]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 삭제 (논리적 삭제 - is_active = 0)
|
||||
*/
|
||||
async delete(id, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_types SET is_active = 0, updated_at = NOW() WHERE id = ?`;
|
||||
const [result] = await db.query(query, [id]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 우선순위 업데이트
|
||||
*/
|
||||
async updatePriority(id, priority, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_types SET priority = ?, updated_at = NOW() WHERE id = ?`;
|
||||
const [result] = await db.query(query, [priority, id]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationTypeModel;
|
||||
505
deploy/tkfb-package/api.hyungi.net/models/visitRequestModel.js
Normal file
505
deploy/tkfb-package/api.hyungi.net/models/visitRequestModel.js
Normal file
@@ -0,0 +1,505 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// ==================== 출입 신청 관리 ====================
|
||||
|
||||
/**
|
||||
* 출입 신청 생성
|
||||
*/
|
||||
const createVisitRequest = async (requestData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
requester_id,
|
||||
visitor_company,
|
||||
visitor_count = 1,
|
||||
category_id,
|
||||
workplace_id,
|
||||
visit_date,
|
||||
visit_time,
|
||||
purpose_id,
|
||||
notes = null
|
||||
} = requestData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workplace_visit_requests
|
||||
(requester_id, visitor_company, visitor_count, category_id, workplace_id,
|
||||
visit_date, visit_time, purpose_id, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[requester_id, visitor_company, visitor_count, category_id, workplace_id,
|
||||
visit_date, visit_time, purpose_id, notes]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 목록 조회 (필터 옵션 포함)
|
||||
*/
|
||||
const getAllVisitRequests = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
vr.request_id, vr.requester_id, vr.visitor_company, vr.visitor_count,
|
||||
vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time,
|
||||
vr.purpose_id, vr.notes, vr.status,
|
||||
vr.approved_by, vr.approved_at, vr.rejection_reason,
|
||||
vr.created_at, vr.updated_at,
|
||||
u.username as requester_name, u.name as requester_full_name,
|
||||
wc.category_name, w.workplace_name,
|
||||
vpt.purpose_name,
|
||||
approver.username as approver_name
|
||||
FROM workplace_visit_requests vr
|
||||
INNER JOIN users u ON vr.requester_id = u.user_id
|
||||
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
||||
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
||||
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
||||
LEFT JOIN users approver ON vr.approved_by = approver.user_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
// 필터 적용
|
||||
if (filters.status) {
|
||||
query += ` AND vr.status = ?`;
|
||||
params.push(filters.status);
|
||||
}
|
||||
|
||||
if (filters.visit_date) {
|
||||
query += ` AND vr.visit_date = ?`;
|
||||
params.push(filters.visit_date);
|
||||
}
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
query += ` AND vr.visit_date BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.requester_id) {
|
||||
query += ` AND vr.requester_id = ?`;
|
||||
params.push(filters.requester_id);
|
||||
}
|
||||
|
||||
if (filters.category_id) {
|
||||
query += ` AND vr.category_id = ?`;
|
||||
params.push(filters.category_id);
|
||||
}
|
||||
|
||||
query += ` ORDER BY vr.visit_date DESC, vr.visit_time DESC, vr.created_at DESC`;
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 상세 조회
|
||||
*/
|
||||
const getVisitRequestById = async (requestId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
vr.request_id, vr.requester_id, vr.visitor_company, vr.visitor_count,
|
||||
vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time,
|
||||
vr.purpose_id, vr.notes, vr.status,
|
||||
vr.approved_by, vr.approved_at, vr.rejection_reason,
|
||||
vr.created_at, vr.updated_at,
|
||||
u.username as requester_name, u.name as requester_full_name,
|
||||
wc.category_name, w.workplace_name,
|
||||
vpt.purpose_name,
|
||||
approver.username as approver_name
|
||||
FROM workplace_visit_requests vr
|
||||
INNER JOIN users u ON vr.requester_id = u.user_id
|
||||
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
||||
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
||||
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
||||
LEFT JOIN users approver ON vr.approved_by = approver.user_id
|
||||
WHERE vr.request_id = ?`,
|
||||
[requestId]
|
||||
);
|
||||
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 수정
|
||||
*/
|
||||
const updateVisitRequest = async (requestId, requestData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
visitor_company,
|
||||
visitor_count,
|
||||
category_id,
|
||||
workplace_id,
|
||||
visit_date,
|
||||
visit_time,
|
||||
purpose_id,
|
||||
notes
|
||||
} = requestData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_visit_requests
|
||||
SET visitor_company = ?, visitor_count = ?, category_id = ?, workplace_id = ?,
|
||||
visit_date = ?, visit_time = ?, purpose_id = ?, notes = ?, updated_at = NOW()
|
||||
WHERE request_id = ?`,
|
||||
[visitor_company, visitor_count, category_id, workplace_id,
|
||||
visit_date, visit_time, purpose_id, notes, requestId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 삭제
|
||||
*/
|
||||
const deleteVisitRequest = async (requestId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM workplace_visit_requests WHERE request_id = ?`,
|
||||
[requestId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 승인
|
||||
*/
|
||||
const approveVisitRequest = async (requestId, approvedBy, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_visit_requests
|
||||
SET status = 'approved', approved_by = ?, approved_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = ?`,
|
||||
[approvedBy, requestId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 반려
|
||||
*/
|
||||
const rejectVisitRequest = async (requestId, rejectionData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { approved_by, rejection_reason } = rejectionData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_visit_requests
|
||||
SET status = 'rejected', approved_by = ?, approved_at = NOW(),
|
||||
rejection_reason = ?, updated_at = NOW()
|
||||
WHERE request_id = ?`,
|
||||
[approved_by, rejection_reason, requestId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 상태 변경
|
||||
*/
|
||||
const updateVisitRequestStatus = async (requestId, status, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_visit_requests
|
||||
SET status = ?, updated_at = NOW()
|
||||
WHERE request_id = ?`,
|
||||
[status, requestId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 방문 목적 관리 ====================
|
||||
|
||||
/**
|
||||
* 모든 방문 목적 조회
|
||||
*/
|
||||
const getAllVisitPurposes = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
|
||||
FROM visit_purpose_types
|
||||
ORDER BY display_order ASC, purpose_id ASC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 활성 방문 목적만 조회
|
||||
*/
|
||||
const getActiveVisitPurposes = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
|
||||
FROM visit_purpose_types
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY display_order ASC, purpose_id ASC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 추가
|
||||
*/
|
||||
const createVisitPurpose = async (purposeData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { purpose_name, display_order = 0, is_active = true } = purposeData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO visit_purpose_types (purpose_name, display_order, is_active)
|
||||
VALUES (?, ?, ?)`,
|
||||
[purpose_name, display_order, is_active]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 수정
|
||||
*/
|
||||
const updateVisitPurpose = async (purposeId, purposeData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { purpose_name, display_order, is_active } = purposeData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE visit_purpose_types
|
||||
SET purpose_name = ?, display_order = ?, is_active = ?
|
||||
WHERE purpose_id = ?`,
|
||||
[purpose_name, display_order, is_active, purposeId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 삭제
|
||||
*/
|
||||
const deleteVisitPurpose = async (purposeId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM visit_purpose_types WHERE purpose_id = ?`,
|
||||
[purposeId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 안전교육 기록 관리 ====================
|
||||
|
||||
/**
|
||||
* 안전교육 기록 생성
|
||||
*/
|
||||
const createTrainingRecord = async (trainingData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
request_id,
|
||||
trainer_id,
|
||||
training_date,
|
||||
training_start_time,
|
||||
training_end_time = null,
|
||||
training_topics = null
|
||||
} = trainingData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO safety_training_records
|
||||
(request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 출입 신청의 안전교육 기록 조회
|
||||
*/
|
||||
const getTrainingRecordByRequestId = async (requestId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
str.training_id, str.request_id, str.trainer_id, str.training_date,
|
||||
str.training_start_time, str.training_end_time, str.training_topics,
|
||||
str.signature_data, str.completed_at, str.created_at, str.updated_at,
|
||||
u.username as trainer_name, u.name as trainer_full_name
|
||||
FROM safety_training_records str
|
||||
INNER JOIN users u ON str.trainer_id = u.user_id
|
||||
WHERE str.request_id = ?`,
|
||||
[requestId]
|
||||
);
|
||||
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 기록 수정
|
||||
*/
|
||||
const updateTrainingRecord = async (trainingId, trainingData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
training_date,
|
||||
training_start_time,
|
||||
training_end_time,
|
||||
training_topics
|
||||
} = trainingData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE safety_training_records
|
||||
SET training_date = ?, training_start_time = ?, training_end_time = ?,
|
||||
training_topics = ?, updated_at = NOW()
|
||||
WHERE training_id = ?`,
|
||||
[training_date, training_start_time, training_end_time, training_topics, trainingId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 완료 (서명 포함)
|
||||
*/
|
||||
const completeTraining = async (trainingId, signatureData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE safety_training_records
|
||||
SET signature_data = ?, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE training_id = ?`,
|
||||
[signatureData, trainingId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 목록 조회 (날짜별 필터)
|
||||
*/
|
||||
const getTrainingRecords = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
str.training_id, str.request_id, str.trainer_id, str.training_date,
|
||||
str.training_start_time, str.training_end_time, str.training_topics,
|
||||
str.completed_at, str.created_at, str.updated_at,
|
||||
u.username as trainer_name, u.name as trainer_full_name,
|
||||
vr.visitor_company, vr.visitor_count, vr.visit_date
|
||||
FROM safety_training_records str
|
||||
INNER JOIN users u ON str.trainer_id = u.user_id
|
||||
INNER JOIN workplace_visit_requests vr ON str.request_id = vr.request_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
if (filters.training_date) {
|
||||
query += ` AND str.training_date = ?`;
|
||||
params.push(filters.training_date);
|
||||
}
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
query += ` AND str.training_date BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.trainer_id) {
|
||||
query += ` AND str.trainer_id = ?`;
|
||||
params.push(filters.trainer_id);
|
||||
}
|
||||
|
||||
query += ` ORDER BY str.training_date DESC, str.training_start_time DESC`;
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// 출입 신청
|
||||
createVisitRequest,
|
||||
getAllVisitRequests,
|
||||
getVisitRequestById,
|
||||
updateVisitRequest,
|
||||
deleteVisitRequest,
|
||||
approveVisitRequest,
|
||||
rejectVisitRequest,
|
||||
updateVisitRequestStatus,
|
||||
|
||||
// 방문 목적
|
||||
getAllVisitPurposes,
|
||||
getActiveVisitPurposes,
|
||||
createVisitPurpose,
|
||||
updateVisitPurpose,
|
||||
deleteVisitPurpose,
|
||||
|
||||
// 안전교육
|
||||
createTrainingRecord,
|
||||
getTrainingRecordByRequestId,
|
||||
updateTrainingRecord,
|
||||
completeTraining,
|
||||
getTrainingRecords
|
||||
};
|
||||
887
deploy/tkfb-package/api.hyungi.net/models/workIssueModel.js
Normal file
887
deploy/tkfb-package/api.hyungi.net/models/workIssueModel.js
Normal file
@@ -0,0 +1,887 @@
|
||||
/**
|
||||
* 작업 중 문제 신고 모델
|
||||
* 부적합/안전 신고 관련 DB 쿼리
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// ==================== 신고 카테고리 관리 ====================
|
||||
|
||||
/**
|
||||
* 모든 신고 카테고리 조회
|
||||
*/
|
||||
const getAllCategories = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_type, category_name, description, display_order, is_active, created_at
|
||||
FROM issue_report_categories
|
||||
ORDER BY category_type, display_order, category_id`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 타입별 활성 카테고리 조회 (nonconformity/safety)
|
||||
*/
|
||||
const getCategoriesByType = async (categoryType, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_type, category_name, description, display_order
|
||||
FROM issue_report_categories
|
||||
WHERE category_type = ? AND is_active = TRUE
|
||||
ORDER BY display_order, category_id`,
|
||||
[categoryType]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 생성
|
||||
*/
|
||||
const createCategory = async (categoryData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { category_type, category_name, description = null, display_order = 0 } = categoryData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO issue_report_categories (category_type, category_name, description, display_order)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[category_type, category_name, description, display_order]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 수정
|
||||
*/
|
||||
const updateCategory = async (categoryId, categoryData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { category_name, description, display_order, is_active } = categoryData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE issue_report_categories
|
||||
SET category_name = ?, description = ?, display_order = ?, is_active = ?
|
||||
WHERE category_id = ?`,
|
||||
[category_name, description, display_order, is_active, categoryId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 삭제
|
||||
*/
|
||||
const deleteCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM issue_report_categories WHERE category_id = ?`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 사전 정의 신고 항목 관리 ====================
|
||||
|
||||
/**
|
||||
* 카테고리별 활성 항목 조회
|
||||
*/
|
||||
const getItemsByCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT item_id, category_id, item_name, description, severity, display_order
|
||||
FROM issue_report_items
|
||||
WHERE category_id = ? AND is_active = TRUE
|
||||
ORDER BY display_order, item_id`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 항목 조회 (관리용)
|
||||
*/
|
||||
const getAllItems = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT iri.item_id, iri.category_id, iri.item_name, iri.description,
|
||||
iri.severity, iri.display_order, iri.is_active, iri.created_at,
|
||||
irc.category_name, irc.category_type
|
||||
FROM issue_report_items iri
|
||||
INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id
|
||||
ORDER BY irc.category_type, irc.display_order, iri.display_order, iri.item_id`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 생성
|
||||
*/
|
||||
const createItem = async (itemData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { category_id, item_name, description = null, severity = 'medium', display_order = 0 } = itemData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[category_id, item_name, description, severity, display_order]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 수정
|
||||
*/
|
||||
const updateItem = async (itemId, itemData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { item_name, description, severity, display_order, is_active } = itemData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE issue_report_items
|
||||
SET item_name = ?, description = ?, severity = ?, display_order = ?, is_active = ?
|
||||
WHERE item_id = ?`,
|
||||
[item_name, description, severity, display_order, is_active, itemId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 항목 삭제
|
||||
*/
|
||||
const deleteItem = async (itemId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM issue_report_items WHERE item_id = ?`,
|
||||
[itemId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 문제 신고 관리 ====================
|
||||
|
||||
// 한국 시간 유틸리티 import
|
||||
const { getKoreaDatetime } = require('../utils/dateUtils');
|
||||
|
||||
/**
|
||||
* 신고 생성
|
||||
*/
|
||||
const createReport = async (reportData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
reporter_id,
|
||||
factory_category_id = null,
|
||||
workplace_id = null,
|
||||
custom_location = null,
|
||||
tbm_session_id = null,
|
||||
visit_request_id = null,
|
||||
issue_category_id,
|
||||
issue_item_id = null,
|
||||
additional_description = null,
|
||||
photo_path1 = null,
|
||||
photo_path2 = null,
|
||||
photo_path3 = null,
|
||||
photo_path4 = null,
|
||||
photo_path5 = null
|
||||
} = reportData;
|
||||
|
||||
// 한국 시간 기준으로 신고 일시 설정
|
||||
const reportDate = getKoreaDatetime();
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO work_issue_reports
|
||||
(reporter_id, report_date, factory_category_id, workplace_id, custom_location,
|
||||
tbm_session_id, visit_request_id, issue_category_id, issue_item_id,
|
||||
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[reporter_id, reportDate, factory_category_id, workplace_id, custom_location,
|
||||
tbm_session_id, visit_request_id, issue_category_id, issue_item_id,
|
||||
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5]
|
||||
);
|
||||
|
||||
// 상태 변경 로그 기록
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
|
||||
VALUES (?, NULL, 'reported', ?)`,
|
||||
[result.insertId, reporter_id]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 목록 조회 (필터 옵션 포함)
|
||||
*/
|
||||
const getAllReports = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
wir.report_id, wir.reporter_id, wir.report_date,
|
||||
wir.factory_category_id, wir.workplace_id, wir.custom_location,
|
||||
wir.tbm_session_id, wir.visit_request_id,
|
||||
wir.issue_category_id, wir.issue_item_id, wir.additional_description,
|
||||
wir.photo_path1, wir.photo_path2, wir.photo_path3, wir.photo_path4, wir.photo_path5,
|
||||
wir.status, wir.assigned_department, wir.assigned_user_id, wir.assigned_at,
|
||||
wir.resolution_notes, wir.resolved_at,
|
||||
wir.created_at, wir.updated_at,
|
||||
u.username as reporter_name, u.name as reporter_full_name,
|
||||
wc.category_name as factory_name,
|
||||
w.workplace_name,
|
||||
irc.category_type, irc.category_name as issue_category_name,
|
||||
iri.item_name as issue_item_name, iri.severity,
|
||||
assignee.username as assigned_user_name, assignee.name as assigned_full_name
|
||||
FROM work_issue_reports wir
|
||||
INNER JOIN users u ON wir.reporter_id = u.user_id
|
||||
LEFT JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
|
||||
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
|
||||
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
|
||||
LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id
|
||||
LEFT JOIN users assignee ON wir.assigned_user_id = assignee.user_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
// 필터 적용
|
||||
if (filters.status) {
|
||||
query += ` AND wir.status = ?`;
|
||||
params.push(filters.status);
|
||||
}
|
||||
|
||||
if (filters.category_type) {
|
||||
query += ` AND irc.category_type = ?`;
|
||||
params.push(filters.category_type);
|
||||
}
|
||||
|
||||
if (filters.issue_category_id) {
|
||||
query += ` AND wir.issue_category_id = ?`;
|
||||
params.push(filters.issue_category_id);
|
||||
}
|
||||
|
||||
if (filters.factory_category_id) {
|
||||
query += ` AND wir.factory_category_id = ?`;
|
||||
params.push(filters.factory_category_id);
|
||||
}
|
||||
|
||||
if (filters.workplace_id) {
|
||||
query += ` AND wir.workplace_id = ?`;
|
||||
params.push(filters.workplace_id);
|
||||
}
|
||||
|
||||
if (filters.reporter_id) {
|
||||
query += ` AND wir.reporter_id = ?`;
|
||||
params.push(filters.reporter_id);
|
||||
}
|
||||
|
||||
if (filters.assigned_user_id) {
|
||||
query += ` AND wir.assigned_user_id = ?`;
|
||||
params.push(filters.assigned_user_id);
|
||||
}
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
query += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.search) {
|
||||
query += ` AND (wir.additional_description LIKE ? OR iri.item_name LIKE ? OR wir.custom_location LIKE ?)`;
|
||||
const searchTerm = `%${filters.search}%`;
|
||||
params.push(searchTerm, searchTerm, searchTerm);
|
||||
}
|
||||
|
||||
query += ` ORDER BY wir.report_date DESC, wir.report_id DESC`;
|
||||
|
||||
// 페이지네이션
|
||||
if (filters.limit) {
|
||||
query += ` LIMIT ?`;
|
||||
params.push(parseInt(filters.limit));
|
||||
|
||||
if (filters.offset) {
|
||||
query += ` OFFSET ?`;
|
||||
params.push(parseInt(filters.offset));
|
||||
}
|
||||
}
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 상세 조회
|
||||
*/
|
||||
const getReportById = async (reportId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
wir.report_id, wir.reporter_id, wir.report_date,
|
||||
wir.factory_category_id, wir.workplace_id, wir.custom_location,
|
||||
wir.tbm_session_id, wir.visit_request_id,
|
||||
wir.issue_category_id, wir.issue_item_id, wir.additional_description,
|
||||
wir.photo_path1, wir.photo_path2, wir.photo_path3, wir.photo_path4, wir.photo_path5,
|
||||
wir.status, wir.assigned_department, wir.assigned_user_id, wir.assigned_at, wir.assigned_by,
|
||||
wir.resolution_notes, wir.resolution_photo_path1, wir.resolution_photo_path2,
|
||||
wir.resolved_at, wir.resolved_by,
|
||||
wir.modification_history,
|
||||
wir.created_at, wir.updated_at,
|
||||
u.username as reporter_name, u.name as reporter_full_name,
|
||||
wc.category_name as factory_name,
|
||||
w.workplace_name,
|
||||
irc.category_type, irc.category_name as issue_category_name,
|
||||
iri.item_name as issue_item_name, iri.severity,
|
||||
assignee.username as assigned_user_name, assignee.name as assigned_full_name,
|
||||
assigner.username as assigned_by_name,
|
||||
resolver.username as resolved_by_name
|
||||
FROM work_issue_reports wir
|
||||
INNER JOIN users u ON wir.reporter_id = u.user_id
|
||||
LEFT JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
|
||||
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
|
||||
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
|
||||
LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id
|
||||
LEFT JOIN users assignee ON wir.assigned_user_id = assignee.user_id
|
||||
LEFT JOIN users assigner ON wir.assigned_by = assigner.user_id
|
||||
LEFT JOIN users resolver ON wir.resolved_by = resolver.user_id
|
||||
WHERE wir.report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 수정
|
||||
*/
|
||||
const updateReport = async (reportId, reportData, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 기존 데이터 조회
|
||||
const [existing] = await db.query(
|
||||
`SELECT * FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (existing.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
const current = existing[0];
|
||||
|
||||
// 수정 이력 생성
|
||||
const modifications = [];
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const key of Object.keys(reportData)) {
|
||||
if (current[key] !== reportData[key] && reportData[key] !== undefined) {
|
||||
modifications.push({
|
||||
field: key,
|
||||
old_value: current[key],
|
||||
new_value: reportData[key],
|
||||
modified_at: now,
|
||||
modified_by: userId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 이력과 병합
|
||||
const existingHistory = current.modification_history ? JSON.parse(current.modification_history) : [];
|
||||
const newHistory = [...existingHistory, ...modifications];
|
||||
|
||||
const {
|
||||
factory_category_id,
|
||||
workplace_id,
|
||||
custom_location,
|
||||
issue_category_id,
|
||||
issue_item_id,
|
||||
additional_description,
|
||||
photo_path1,
|
||||
photo_path2,
|
||||
photo_path3,
|
||||
photo_path4,
|
||||
photo_path5
|
||||
} = reportData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET factory_category_id = COALESCE(?, factory_category_id),
|
||||
workplace_id = COALESCE(?, workplace_id),
|
||||
custom_location = COALESCE(?, custom_location),
|
||||
issue_category_id = COALESCE(?, issue_category_id),
|
||||
issue_item_id = COALESCE(?, issue_item_id),
|
||||
additional_description = COALESCE(?, additional_description),
|
||||
photo_path1 = COALESCE(?, photo_path1),
|
||||
photo_path2 = COALESCE(?, photo_path2),
|
||||
photo_path3 = COALESCE(?, photo_path3),
|
||||
photo_path4 = COALESCE(?, photo_path4),
|
||||
photo_path5 = COALESCE(?, photo_path5),
|
||||
modification_history = ?,
|
||||
updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[factory_category_id, workplace_id, custom_location,
|
||||
issue_category_id, issue_item_id, additional_description,
|
||||
photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
|
||||
JSON.stringify(newHistory), reportId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 삭제
|
||||
*/
|
||||
const deleteReport = async (reportId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 먼저 사진 경로 조회 (삭제용)
|
||||
const [photos] = await db.query(
|
||||
`SELECT photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
|
||||
resolution_photo_path1, resolution_photo_path2
|
||||
FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
// 삭제할 사진 경로 반환
|
||||
callback(null, { result, photos: photos[0] });
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 상태 관리 ====================
|
||||
|
||||
/**
|
||||
* 신고 접수 (reported → received)
|
||||
*/
|
||||
const receiveReport = async (reportId, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
if (current[0].status !== 'reported') {
|
||||
return callback(new Error('접수 대기 상태가 아닙니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET status = 'received', updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
// 상태 변경 로그
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
|
||||
VALUES (?, 'reported', 'received', ?)`,
|
||||
[reportId, userId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 담당자 배정
|
||||
*/
|
||||
const assignReport = async (reportId, assignData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { assigned_department, assigned_user_id, assigned_by } = assignData;
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
// 접수 상태 이상이어야 배정 가능
|
||||
const validStatuses = ['received', 'in_progress'];
|
||||
if (!validStatuses.includes(current[0].status)) {
|
||||
return callback(new Error('접수된 상태에서만 담당자 배정이 가능합니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET assigned_department = ?, assigned_user_id = ?,
|
||||
assigned_at = NOW(), assigned_by = ?, updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[assigned_department, assigned_user_id, assigned_by, reportId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 처리 시작 (received → in_progress)
|
||||
*/
|
||||
const startProcessing = async (reportId, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
if (current[0].status !== 'received') {
|
||||
return callback(new Error('접수된 상태에서만 처리를 시작할 수 있습니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET status = 'in_progress', updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
// 상태 변경 로그
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
|
||||
VALUES (?, 'received', 'in_progress', ?)`,
|
||||
[reportId, userId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 처리 완료 (in_progress → completed)
|
||||
*/
|
||||
const completeReport = async (reportId, completionData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by } = completionData;
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
if (current[0].status !== 'in_progress') {
|
||||
return callback(new Error('처리 중 상태에서만 완료할 수 있습니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET status = 'completed', resolution_notes = ?,
|
||||
resolution_photo_path1 = ?, resolution_photo_path2 = ?,
|
||||
resolved_at = NOW(), resolved_by = ?, updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by, reportId]
|
||||
);
|
||||
|
||||
// 상태 변경 로그
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by, change_reason)
|
||||
VALUES (?, 'in_progress', 'completed', ?, ?)`,
|
||||
[reportId, resolved_by, resolution_notes]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 신고 종료 (completed → closed)
|
||||
*/
|
||||
const closeReport = async (reportId, userId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
// 현재 상태 확인
|
||||
const [current] = await db.query(
|
||||
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
if (current.length === 0) {
|
||||
return callback(new Error('신고를 찾을 수 없습니다.'));
|
||||
}
|
||||
|
||||
if (current[0].status !== 'completed') {
|
||||
return callback(new Error('완료된 상태에서만 종료할 수 있습니다.'));
|
||||
}
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE work_issue_reports
|
||||
SET status = 'closed', updated_at = NOW()
|
||||
WHERE report_id = ?`,
|
||||
[reportId]
|
||||
);
|
||||
|
||||
// 상태 변경 로그
|
||||
await db.query(
|
||||
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
|
||||
VALUES (?, 'completed', 'closed', ?)`,
|
||||
[reportId, userId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 상태 변경 이력 조회
|
||||
*/
|
||||
const getStatusLogs = async (reportId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT wisl.log_id, wisl.report_id, wisl.previous_status, wisl.new_status,
|
||||
wisl.changed_by, wisl.change_reason, wisl.changed_at,
|
||||
u.username as changed_by_name, u.name as changed_by_full_name
|
||||
FROM work_issue_status_logs wisl
|
||||
INNER JOIN users u ON wisl.changed_by = u.user_id
|
||||
WHERE wisl.report_id = ?
|
||||
ORDER BY wisl.changed_at ASC`,
|
||||
[reportId]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 통계 ====================
|
||||
|
||||
/**
|
||||
* 신고 통계 요약
|
||||
*/
|
||||
const getStatsSummary = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
whereClause += ` AND DATE(report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.factory_category_id) {
|
||||
whereClause += ` AND factory_category_id = ?`;
|
||||
params.push(filters.factory_category_id);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'reported' THEN 1 ELSE 0 END) as reported,
|
||||
SUM(CASE WHEN status = 'received' THEN 1 ELSE 0 END) as received,
|
||||
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
|
||||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
||||
SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed
|
||||
FROM work_issue_reports
|
||||
WHERE ${whereClause}`,
|
||||
params
|
||||
);
|
||||
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리별 통계
|
||||
*/
|
||||
const getStatsByCategory = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
let whereClause = '1=1';
|
||||
const params = [];
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
irc.category_type, irc.category_name,
|
||||
COUNT(*) as count
|
||||
FROM work_issue_reports wir
|
||||
INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY irc.category_id
|
||||
ORDER BY irc.category_type, count DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장별 통계
|
||||
*/
|
||||
const getStatsByWorkplace = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
let whereClause = 'wir.workplace_id IS NOT NULL';
|
||||
const params = [];
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.factory_category_id) {
|
||||
whereClause += ` AND wir.factory_category_id = ?`;
|
||||
params.push(filters.factory_category_id);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
wir.factory_category_id, wc.category_name as factory_name,
|
||||
wir.workplace_id, w.workplace_name,
|
||||
COUNT(*) as count
|
||||
FROM work_issue_reports wir
|
||||
INNER JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id
|
||||
INNER JOIN workplaces w ON wir.workplace_id = w.workplace_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY wir.factory_category_id, wir.workplace_id
|
||||
ORDER BY count DESC`,
|
||||
params
|
||||
);
|
||||
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// 카테고리
|
||||
getAllCategories,
|
||||
getCategoriesByType,
|
||||
createCategory,
|
||||
updateCategory,
|
||||
deleteCategory,
|
||||
|
||||
// 항목
|
||||
getItemsByCategory,
|
||||
getAllItems,
|
||||
createItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
|
||||
// 신고
|
||||
createReport,
|
||||
getAllReports,
|
||||
getReportById,
|
||||
updateReport,
|
||||
deleteReport,
|
||||
|
||||
// 상태 관리
|
||||
receiveReport,
|
||||
assignReport,
|
||||
startProcessing,
|
||||
completeReport,
|
||||
closeReport,
|
||||
getStatusLogs,
|
||||
|
||||
// 통계
|
||||
getStatsSummary,
|
||||
getStatsByCategory,
|
||||
getStatsByWorkplace
|
||||
};
|
||||
223
deploy/tkfb-package/api.hyungi.net/models/workReportModel.js
Normal file
223
deploy/tkfb-package/api.hyungi.net/models/workReportModel.js
Normal file
@@ -0,0 +1,223 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
/**
|
||||
* 1. 여러 건 등록 (트랜잭션 사용)
|
||||
*/
|
||||
const createBatch = async (reports, callback) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
const sql = `
|
||||
INSERT INTO WorkReports
|
||||
(\`date\`, worker_id, project_id, task_id, overtime_hours, work_details, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
for (const rpt of reports) {
|
||||
const params = [
|
||||
rpt.date,
|
||||
rpt.worker_id,
|
||||
rpt.project_id,
|
||||
rpt.task_id || null,
|
||||
rpt.overtime_hours || null,
|
||||
rpt.work_details || null,
|
||||
rpt.memo || null
|
||||
];
|
||||
await conn.query(sql, params);
|
||||
}
|
||||
|
||||
await conn.commit();
|
||||
callback(null);
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
callback(err);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. 단일 등록
|
||||
*/
|
||||
const create = async (report, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
date, worker_id, project_id,
|
||||
task_id, overtime_hours,
|
||||
work_details, memo
|
||||
} = report;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO WorkReports
|
||||
(\`date\`, worker_id, project_id, task_id, overtime_hours, work_details, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
date,
|
||||
worker_id,
|
||||
project_id,
|
||||
task_id || null,
|
||||
overtime_hours || null,
|
||||
work_details || null,
|
||||
memo || null
|
||||
]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 3. 날짜별 조회
|
||||
*/
|
||||
const getAllByDate = async (date, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const sql = `
|
||||
SELECT
|
||||
wr.worker_id, -- 이 줄을 추가했습니다
|
||||
wr.id,
|
||||
wr.\`date\`,
|
||||
w.worker_name,
|
||||
p.project_name,
|
||||
CONCAT(t.category, ':', t.subcategory) AS task_name,
|
||||
wr.overtime_hours,
|
||||
wr.work_details,
|
||||
wr.memo
|
||||
FROM WorkReports wr
|
||||
LEFT JOIN workers w ON wr.worker_id = w.worker_id
|
||||
LEFT JOIN projects p ON wr.project_id = p.project_id
|
||||
LEFT JOIN Tasks t ON wr.task_id = t.task_id
|
||||
WHERE wr.\`date\` = ?
|
||||
ORDER BY w.worker_name ASC
|
||||
`;
|
||||
const [rows] = await db.query(sql, [date]);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 4. 기간 조회
|
||||
*/
|
||||
const getByRange = async (start, end, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT id, \`date\`, worker_id, project_id, morning_task_id, afternoon_task_id, overtime_hours, overtime_task_id, work_details, note, memo, created_at, updated_at, morning_project_id, afternoon_project_id, overtime_project_id, task_id FROM WorkReports
|
||||
WHERE \`date\` BETWEEN ? AND ?
|
||||
ORDER BY \`date\` ASC`,
|
||||
[start, end]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 5. ID로 조회
|
||||
*/
|
||||
const getById = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT id, \`date\`, worker_id, project_id, morning_task_id, afternoon_task_id, overtime_hours, overtime_task_id, work_details, note, memo, created_at, updated_at, morning_project_id, afternoon_project_id, overtime_project_id, task_id FROM WorkReports WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 6. 수정
|
||||
*/
|
||||
const update = async (id, report, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
date, worker_id, project_id,
|
||||
task_id, overtime_hours,
|
||||
work_details, memo
|
||||
} = report;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE WorkReports
|
||||
SET \`date\` = ?,
|
||||
worker_id = ?,
|
||||
project_id = ?,
|
||||
task_id = ?,
|
||||
overtime_hours = ?,
|
||||
work_details = ?,
|
||||
memo = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?`,
|
||||
[
|
||||
date,
|
||||
worker_id,
|
||||
project_id,
|
||||
task_id || null,
|
||||
overtime_hours || null,
|
||||
work_details || null,
|
||||
memo || null,
|
||||
id
|
||||
]
|
||||
);
|
||||
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 7. 삭제
|
||||
*/
|
||||
const remove = async (id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM WorkReports WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
callback(null, result.affectedRows);
|
||||
} catch (err) {
|
||||
callback(new Error(err.message || String(err)));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 8. 중복 확인
|
||||
*/
|
||||
const existsByDateAndWorker = async (date, worker_id, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT 1 FROM WorkReports WHERE \`date\` = ? AND worker_id = ? LIMIT 1`,
|
||||
[date, worker_id]
|
||||
);
|
||||
callback(null, rows.length > 0);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ 내보내기
|
||||
module.exports = {
|
||||
create,
|
||||
createBatch,
|
||||
getAllByDate,
|
||||
getByRange,
|
||||
getById,
|
||||
update,
|
||||
remove,
|
||||
existsByDateAndWorker
|
||||
};
|
||||
197
deploy/tkfb-package/api.hyungi.net/models/workerModel.js
Normal file
197
deploy/tkfb-package/api.hyungi.net/models/workerModel.js
Normal file
@@ -0,0 +1,197 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// 날짜 형식 변환 헬퍼 함수 (ISO 8601 -> MySQL DATE)
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return null;
|
||||
if (typeof dateStr === 'string' && dateStr.includes('T')) {
|
||||
return dateStr.split('T')[0]; // ISO 8601 -> YYYY-MM-DD
|
||||
}
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
// 1. 작업자 생성
|
||||
const create = async (worker) => {
|
||||
const db = await getDb();
|
||||
const {
|
||||
worker_name,
|
||||
job_type = null,
|
||||
join_date = null,
|
||||
salary = null,
|
||||
annual_leave = null,
|
||||
status = 'active',
|
||||
employment_status = 'employed',
|
||||
department_id = null
|
||||
} = worker;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workers
|
||||
(worker_name, job_type, join_date, salary, annual_leave, status, employment_status, department_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[worker_name, job_type, formatDate(join_date), salary, annual_leave, status, employment_status, department_id]
|
||||
);
|
||||
|
||||
return result.insertId;
|
||||
};
|
||||
|
||||
// 2. 전체 조회
|
||||
const getAll = async () => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT
|
||||
w.*,
|
||||
CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active,
|
||||
u.user_id,
|
||||
d.department_name
|
||||
FROM workers w
|
||||
LEFT JOIN users u ON w.worker_id = u.worker_id
|
||||
LEFT JOIN departments d ON w.department_id = d.department_id
|
||||
ORDER BY w.worker_id DESC
|
||||
`);
|
||||
return rows;
|
||||
};
|
||||
|
||||
// 3. 단일 조회
|
||||
const getById = async (worker_id) => {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(`
|
||||
SELECT
|
||||
w.*,
|
||||
CASE WHEN w.status = 'active' THEN 1 ELSE 0 END AS is_active,
|
||||
u.user_id,
|
||||
d.department_name
|
||||
FROM workers w
|
||||
LEFT JOIN users u ON w.worker_id = u.worker_id
|
||||
LEFT JOIN departments d ON w.department_id = d.department_id
|
||||
WHERE w.worker_id = ?
|
||||
`, [worker_id]);
|
||||
return rows[0];
|
||||
};
|
||||
|
||||
// 4. 작업자 수정
|
||||
const update = async (worker) => {
|
||||
const db = await getDb();
|
||||
const {
|
||||
worker_id,
|
||||
worker_name,
|
||||
job_type,
|
||||
status,
|
||||
join_date,
|
||||
salary,
|
||||
annual_leave,
|
||||
employment_status,
|
||||
department_id
|
||||
} = worker;
|
||||
|
||||
// 업데이트할 필드만 동적으로 구성
|
||||
const updates = [];
|
||||
const values = [];
|
||||
|
||||
if (worker_name !== undefined) {
|
||||
updates.push('worker_name = ?');
|
||||
values.push(worker_name);
|
||||
}
|
||||
if (job_type !== undefined) {
|
||||
updates.push('job_type = ?');
|
||||
values.push(job_type);
|
||||
}
|
||||
if (status !== undefined) {
|
||||
updates.push('status = ?');
|
||||
values.push(status);
|
||||
}
|
||||
if (join_date !== undefined) {
|
||||
updates.push('join_date = ?');
|
||||
values.push(formatDate(join_date));
|
||||
}
|
||||
if (salary !== undefined) {
|
||||
updates.push('salary = ?');
|
||||
values.push(salary);
|
||||
}
|
||||
if (annual_leave !== undefined) {
|
||||
updates.push('annual_leave = ?');
|
||||
values.push(annual_leave);
|
||||
}
|
||||
if (employment_status !== undefined) {
|
||||
updates.push('employment_status = ?');
|
||||
values.push(employment_status);
|
||||
}
|
||||
if (department_id !== undefined) {
|
||||
updates.push('department_id = ?');
|
||||
values.push(department_id);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
throw new Error('업데이트할 필드가 없습니다');
|
||||
}
|
||||
|
||||
values.push(worker_id); // WHERE 조건용
|
||||
|
||||
const query = `UPDATE workers SET ${updates.join(', ')} WHERE worker_id = ?`;
|
||||
|
||||
console.log('🔍 실행할 SQL:', query);
|
||||
console.log('🔍 SQL 파라미터:', values);
|
||||
|
||||
const [result] = await db.query(query, values);
|
||||
|
||||
return result.affectedRows;
|
||||
};
|
||||
|
||||
// 5. 삭제 (외래키 제약조건 처리)
|
||||
const remove = async (worker_id) => {
|
||||
const db = await getDb();
|
||||
const conn = await db.getConnection();
|
||||
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
console.log(`🗑️ 작업자 삭제 시작: worker_id=${worker_id}`);
|
||||
|
||||
// 안전한 삭제: 각 테이블을 개별적으로 처리하고 오류가 발생해도 계속 진행
|
||||
const tables = [
|
||||
{ name: 'users', query: 'UPDATE users SET worker_id = NULL WHERE worker_id = ?', action: '업데이트' },
|
||||
{ name: 'Users', query: 'UPDATE Users SET worker_id = NULL WHERE worker_id = ?', action: '업데이트' },
|
||||
{ name: 'daily_issue_reports', query: 'DELETE FROM daily_issue_reports WHERE worker_id = ?', action: '삭제' },
|
||||
{ name: 'DailyIssueReports', query: 'DELETE FROM DailyIssueReports WHERE worker_id = ?', action: '삭제' },
|
||||
{ name: 'work_reports', query: 'DELETE FROM work_reports WHERE worker_id = ?', action: '삭제' },
|
||||
{ name: 'WorkReports', query: 'DELETE FROM WorkReports WHERE worker_id = ?', action: '삭제' },
|
||||
{ name: 'daily_work_reports', query: 'DELETE FROM daily_work_reports WHERE worker_id = ?', action: '삭제' },
|
||||
{ name: 'monthly_worker_status', query: 'DELETE FROM monthly_worker_status WHERE worker_id = ?', action: '삭제' },
|
||||
{ name: 'worker_groups', query: 'DELETE FROM worker_groups WHERE worker_id = ?', action: '삭제' }
|
||||
];
|
||||
|
||||
for (const table of tables) {
|
||||
try {
|
||||
const [result] = await conn.query(table.query, [worker_id]);
|
||||
if (result.affectedRows > 0) {
|
||||
console.log(`✅ ${table.name} 테이블 ${table.action}: ${result.affectedRows}건`);
|
||||
}
|
||||
} catch (tableError) {
|
||||
console.log(`⚠️ ${table.name} 테이블 ${table.action} 실패 (무시): ${tableError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막으로 작업자 삭제
|
||||
const [result] = await conn.query(
|
||||
`DELETE FROM workers WHERE worker_id = ?`,
|
||||
[worker_id]
|
||||
);
|
||||
console.log(`✅ 작업자 삭제 완료: ${result.affectedRows}건`);
|
||||
|
||||
await conn.commit();
|
||||
return result.affectedRows;
|
||||
|
||||
} catch (err) {
|
||||
await conn.rollback();
|
||||
console.error(`❌ 작업자 삭제 오류 (worker_id: ${worker_id}):`, err);
|
||||
throw new Error(`작업자 삭제 중 오류가 발생했습니다: ${err.message}`);
|
||||
} finally {
|
||||
conn.release();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
getAll,
|
||||
getById,
|
||||
update,
|
||||
remove
|
||||
};
|
||||
440
deploy/tkfb-package/api.hyungi.net/models/workplaceModel.js
Normal file
440
deploy/tkfb-package/api.hyungi.net/models/workplaceModel.js
Normal file
@@ -0,0 +1,440 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// ==================== 카테고리(공장) 관련 ====================
|
||||
|
||||
/**
|
||||
* 카테고리 생성
|
||||
*/
|
||||
const createCategory = async (category, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
category_name,
|
||||
description = null,
|
||||
display_order = 0,
|
||||
is_active = true
|
||||
} = category;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workplace_categories
|
||||
(category_name, description, display_order, is_active)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[category_name, description, display_order, is_active]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 카테고리 조회
|
||||
*/
|
||||
const getAllCategories = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_name, description, display_order, is_active, layout_image, created_at, updated_at
|
||||
FROM workplace_categories
|
||||
ORDER BY display_order ASC, category_id ASC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 활성 카테고리만 조회
|
||||
*/
|
||||
const getActiveCategories = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_name, description, display_order, is_active, layout_image, created_at, updated_at
|
||||
FROM workplace_categories
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY display_order ASC, category_id ASC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ID로 카테고리 조회
|
||||
*/
|
||||
const getCategoryById = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_name, description, display_order, is_active, layout_image, created_at, updated_at
|
||||
FROM workplace_categories
|
||||
WHERE category_id = ?`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 수정
|
||||
*/
|
||||
const updateCategory = async (categoryId, category, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
category_name,
|
||||
description,
|
||||
display_order,
|
||||
is_active,
|
||||
layout_image
|
||||
} = category;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_categories
|
||||
SET category_name = ?, description = ?, display_order = ?, is_active = ?, layout_image = ?, updated_at = NOW()
|
||||
WHERE category_id = ?`,
|
||||
[category_name, description, display_order, is_active, layout_image, categoryId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리 삭제
|
||||
*/
|
||||
const deleteCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM workplace_categories WHERE category_id = ?`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 작업장 관련 ====================
|
||||
|
||||
/**
|
||||
* 작업장 생성
|
||||
*/
|
||||
const createWorkplace = async (workplace, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
category_id = null,
|
||||
workplace_name,
|
||||
description = null,
|
||||
is_active = true,
|
||||
workplace_purpose = null,
|
||||
display_priority = 0
|
||||
} = workplace;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workplaces
|
||||
(category_id, workplace_name, description, is_active, workplace_purpose, display_priority)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[category_id, workplace_name, description, is_active, workplace_purpose, display_priority]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 모든 작업장 조회 (카테고리 정보 포함)
|
||||
*/
|
||||
const getAllWorkplaces = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active, w.workplace_purpose, w.display_priority,
|
||||
w.layout_image, w.created_at, w.updated_at,
|
||||
wc.category_name
|
||||
FROM workplaces w
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
ORDER BY wc.display_order ASC, w.display_priority ASC, w.workplace_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 활성 작업장만 조회
|
||||
*/
|
||||
const getActiveWorkplaces = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active, w.workplace_purpose, w.display_priority,
|
||||
w.layout_image, w.created_at, w.updated_at,
|
||||
wc.category_name
|
||||
FROM workplaces w
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
WHERE w.is_active = TRUE
|
||||
ORDER BY wc.display_order ASC, w.workplace_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리별 작업장 조회
|
||||
*/
|
||||
const getWorkplacesByCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active, w.workplace_purpose, w.display_priority,
|
||||
w.layout_image, w.created_at, w.updated_at,
|
||||
wc.category_name
|
||||
FROM workplaces w
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
WHERE w.category_id = ?
|
||||
ORDER BY w.workplace_id DESC`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ID로 작업장 조회
|
||||
*/
|
||||
const getWorkplaceById = async (workplaceId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active, w.workplace_purpose, w.display_priority,
|
||||
w.layout_image, w.created_at, w.updated_at,
|
||||
wc.category_name
|
||||
FROM workplaces w
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
WHERE w.workplace_id = ?`,
|
||||
[workplaceId]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장 수정
|
||||
*/
|
||||
const updateWorkplace = async (workplaceId, workplace, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
category_id,
|
||||
workplace_name,
|
||||
description,
|
||||
is_active,
|
||||
workplace_purpose,
|
||||
display_priority,
|
||||
layout_image
|
||||
} = workplace;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplaces
|
||||
SET category_id = ?, workplace_name = ?, description = ?, is_active = ?,
|
||||
workplace_purpose = ?, display_priority = ?, layout_image = ?, updated_at = NOW()
|
||||
WHERE workplace_id = ?`,
|
||||
[category_id, workplace_name, description, is_active, workplace_purpose, display_priority, layout_image, workplaceId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장 삭제
|
||||
*/
|
||||
const deleteWorkplace = async (workplaceId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM workplaces WHERE workplace_id = ?`,
|
||||
[workplaceId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 작업장 지도 영역 관련 ====================
|
||||
|
||||
/**
|
||||
* 작업장 지도 영역 생성
|
||||
*/
|
||||
const createMapRegion = async (region, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
workplace_id,
|
||||
category_id,
|
||||
x_start,
|
||||
y_start,
|
||||
x_end,
|
||||
y_end,
|
||||
shape = 'rect',
|
||||
polygon_points = null
|
||||
} = region;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workplace_map_regions
|
||||
(workplace_id, category_id, x_start, y_start, x_end, y_end, shape, polygon_points)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[workplace_id, category_id, x_start, y_start, x_end, y_end, shape, polygon_points]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리(공장)별 지도 영역 조회
|
||||
*/
|
||||
const getMapRegionsByCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT mr.*, w.workplace_name, w.description
|
||||
FROM workplace_map_regions mr
|
||||
INNER JOIN workplaces w ON mr.workplace_id = w.workplace_id
|
||||
WHERE mr.category_id = ? AND w.is_active = TRUE
|
||||
ORDER BY mr.region_id ASC`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장별 지도 영역 조회
|
||||
*/
|
||||
const getMapRegionByWorkplace = async (workplaceId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM workplace_map_regions WHERE workplace_id = ?`,
|
||||
[workplaceId]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 지도 영역 수정
|
||||
*/
|
||||
const updateMapRegion = async (regionId, region, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
x_start,
|
||||
y_start,
|
||||
x_end,
|
||||
y_end,
|
||||
shape,
|
||||
polygon_points
|
||||
} = region;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_map_regions
|
||||
SET x_start = ?, y_start = ?, x_end = ?, y_end = ?, shape = ?, polygon_points = ?, updated_at = NOW()
|
||||
WHERE region_id = ?`,
|
||||
[x_start, y_start, x_end, y_end, shape, polygon_points, regionId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 지도 영역 삭제
|
||||
*/
|
||||
const deleteMapRegion = async (regionId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM workplace_map_regions WHERE region_id = ?`,
|
||||
[regionId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장 영역 일괄 삭제 (카테고리별)
|
||||
*/
|
||||
const deleteMapRegionsByCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM workplace_map_regions WHERE category_id = ?`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// 카테고리
|
||||
createCategory,
|
||||
getAllCategories,
|
||||
getActiveCategories,
|
||||
getCategoryById,
|
||||
updateCategory,
|
||||
deleteCategory,
|
||||
|
||||
// 작업장
|
||||
createWorkplace,
|
||||
getAllWorkplaces,
|
||||
getActiveWorkplaces,
|
||||
getWorkplacesByCategory,
|
||||
getWorkplaceById,
|
||||
updateWorkplace,
|
||||
deleteWorkplace,
|
||||
|
||||
// 지도 영역
|
||||
createMapRegion,
|
||||
getMapRegionsByCategory,
|
||||
getMapRegionByWorkplace,
|
||||
updateMapRegion,
|
||||
deleteMapRegion,
|
||||
deleteMapRegionsByCategory
|
||||
};
|
||||
Reference in New Issue
Block a user