- 공통 유틸리티 추출 (common/utils.js, common/base-state.js) - TBM 모바일 인라인 JS/CSS 외부 파일로 분리 (tbm-mobile.js, tbm-mobile.css) - 미사용 코드 삭제 (index.js, work-report-*.js 등 5개 파일) - TBM/작업보고 state.js, utils.js를 공통 모듈 기반으로 전환 - 작업보고서 SSO 인증 호환 수정 (token/user 함수) - tbmModel.js: incomplete-reports 쿼리에서 users→sso_users 조인 수정, leader_name 조인 추가 - docker-compose.yml: system1-web 볼륨 마운트 추가 - 모바일 인계(handover) 기능 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
855 lines
28 KiB
JavaScript
855 lines
28 KiB
JavaScript
// models/tbmModel.js - TBM 시스템 모델
|
|
const { getDb } = require('../dbPool');
|
|
|
|
const TbmModel = {
|
|
// ==================== TBM 세션 관련 ====================
|
|
|
|
createSession: async (sessionData) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`INSERT INTO tbm_sessions
|
|
(session_date, leader_id, project_id, work_type_id, task_id, work_location, created_by)
|
|
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
|
|
]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
getSessionsByDate: async (date) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`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,
|
|
GROUP_CONCAT(DISTINCT w2.worker_name ORDER BY ta.assignment_id SEPARATOR ', ') as team_member_names,
|
|
(SELECT COUNT(*) FROM tbm_transfers tf
|
|
WHERE (tf.source_session_id = s.session_id OR tf.dest_session_id = s.session_id)
|
|
AND tf.transfer_date = s.session_date) as transfer_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 sso_users u ON s.created_by = u.user_id
|
|
LEFT JOIN tbm_team_assignments ta ON s.session_id = ta.session_id
|
|
LEFT JOIN workers w2 ON ta.worker_id = w2.worker_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`,
|
|
[date]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
getSessionById: async (sessionId) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`SELECT
|
|
s.*,
|
|
w.worker_name as leader_name,
|
|
w.job_type as leader_job_type,
|
|
w.phone_number as leader_phone,
|
|
u.username as created_by_username,
|
|
u.name as created_by_name,
|
|
COUNT(DISTINCT ta.worker_id) as team_member_count,
|
|
first_p.project_name,
|
|
first_p.job_no,
|
|
first_wt.name as work_type_name,
|
|
first_wt.category as work_type_category,
|
|
first_t.task_name,
|
|
first_t.description as task_description,
|
|
first_wp.workplace_name as work_location,
|
|
first_wc.category_name as workplace_category_name
|
|
FROM tbm_sessions s
|
|
LEFT JOIN workers w ON s.leader_id = w.worker_id
|
|
LEFT JOIN sso_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
|
|
LEFT JOIN workplace_categories first_wc ON first_ta.workplace_category_id = first_wc.category_id
|
|
WHERE s.session_id = ?
|
|
GROUP BY s.session_id`,
|
|
[sessionId]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
updateSession: async (sessionId, sessionData) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`UPDATE tbm_sessions
|
|
SET project_id = ?, work_location = ?, status = ?, updated_at = NOW()
|
|
WHERE session_id = ?`,
|
|
[sessionData.project_id, sessionData.work_location, sessionData.status, sessionId]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
completeSession: async (sessionId, endTime) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`UPDATE tbm_sessions
|
|
SET status = 'completed', end_time = ?, updated_at = NOW()
|
|
WHERE session_id = ?`,
|
|
[endTime, sessionId]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
completeSessionWithAttendance: async (sessionId, endTime, attendanceData, createdBy) => {
|
|
const db = await getDb();
|
|
const conn = await db.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
// 1. 세션 정보 조회
|
|
const [sessionRows] = await conn.query(
|
|
'SELECT session_date FROM tbm_sessions WHERE session_id = ?',
|
|
[sessionId]
|
|
);
|
|
if (sessionRows.length === 0) {
|
|
await conn.commit();
|
|
return { affectedRows: 0 };
|
|
}
|
|
const sessionDate = sessionRows[0].session_date;
|
|
let reportDate;
|
|
if (sessionDate instanceof Date) {
|
|
reportDate = sessionDate.toISOString().split('T')[0];
|
|
} else if (typeof sessionDate === 'string') {
|
|
reportDate = sessionDate.split('T')[0];
|
|
} else {
|
|
reportDate = new Date(sessionDate).toISOString().split('T')[0];
|
|
}
|
|
|
|
// 2. 각 작업자의 근태 유형 업데이트
|
|
for (const item of attendanceData) {
|
|
await conn.query(
|
|
`UPDATE tbm_team_assignments
|
|
SET attendance_type = ?, attendance_hours = ?
|
|
WHERE session_id = ? AND worker_id = ?`,
|
|
[item.attendance_type, item.attendance_hours || null, sessionId, item.worker_id]
|
|
);
|
|
}
|
|
|
|
// 3. 연차 작업자 → 작업보고서 자동 생성
|
|
const annualWorkers = attendanceData.filter(a => a.attendance_type === 'annual');
|
|
for (const aw of annualWorkers) {
|
|
const [assignRows] = await conn.query(
|
|
'SELECT assignment_id FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
|
|
[sessionId, aw.worker_id]
|
|
);
|
|
if (assignRows.length > 0) {
|
|
const [existingReport] = await conn.query(
|
|
'SELECT id FROM daily_work_reports WHERE tbm_assignment_id = ?',
|
|
[assignRows[0].assignment_id]
|
|
);
|
|
if (existingReport.length === 0) {
|
|
await conn.query(
|
|
`INSERT INTO daily_work_reports
|
|
(report_date, worker_id, project_id, work_hours, work_status_id, created_by, tbm_assignment_id, created_at)
|
|
VALUES (?, ?, 13, 8, 1, ?, ?, NOW())`,
|
|
[reportDate, aw.worker_id, createdBy, assignRows[0].assignment_id]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. 세션 완료 처리
|
|
await conn.query(
|
|
`UPDATE tbm_sessions
|
|
SET status = 'completed', end_time = ?, updated_at = NOW()
|
|
WHERE session_id = ?`,
|
|
[endTime, sessionId]
|
|
);
|
|
|
|
await conn.commit();
|
|
|
|
// 5. 연차 작업자 근태 동기화
|
|
for (const aw of annualWorkers) {
|
|
try {
|
|
const AttendanceModel = require('./attendanceModel');
|
|
await AttendanceModel.syncWithWorkReports(aw.worker_id, reportDate);
|
|
} catch (syncErr) {
|
|
// 근태 동기화 오류 (무시됨)
|
|
}
|
|
}
|
|
|
|
return { affectedRows: 1 };
|
|
} catch (err) {
|
|
try { await conn.rollback(); } catch (e) {}
|
|
throw err;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
},
|
|
|
|
deleteSession: async (sessionId) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`DELETE FROM tbm_sessions WHERE session_id = ? AND status = 'draft'`,
|
|
[sessionId]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
// ==================== 팀 구성 관련 ====================
|
|
|
|
addTeamMember: async (assignmentData) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`INSERT INTO tbm_team_assignments
|
|
(session_id, worker_id, split_seq, assigned_role, work_detail, is_present, absence_reason,
|
|
project_id, work_type_id, task_id, workplace_category_id, workplace_id, work_hours)
|
|
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),
|
|
work_hours = COALESCE(VALUES(work_hours), work_hours)`,
|
|
[
|
|
assignmentData.session_id,
|
|
assignmentData.worker_id,
|
|
assignmentData.split_seq || 0,
|
|
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,
|
|
assignmentData.work_hours !== undefined ? assignmentData.work_hours : null
|
|
]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
addSplitAssignment: async (assignmentData) => {
|
|
const db = await getDb();
|
|
const [maxRows] = await db.query(
|
|
'SELECT COALESCE(MAX(split_seq), -1) as max_seq FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
|
|
[assignmentData.session_id, assignmentData.worker_id]
|
|
);
|
|
const nextSeq = (maxRows[0].max_seq || 0) + 1;
|
|
|
|
const [result] = await db.query(
|
|
`INSERT INTO tbm_team_assignments
|
|
(session_id, worker_id, split_seq, work_hours, project_id, work_type_id,
|
|
task_id, workplace_category_id, workplace_id, is_present)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`,
|
|
[
|
|
assignmentData.session_id,
|
|
assignmentData.worker_id,
|
|
nextSeq,
|
|
assignmentData.work_hours,
|
|
assignmentData.project_id || null,
|
|
assignmentData.work_type_id || null,
|
|
assignmentData.task_id || null,
|
|
assignmentData.workplace_category_id || null,
|
|
assignmentData.workplace_id || null
|
|
]
|
|
);
|
|
return { assignment_id: result.insertId, split_seq: nextSeq };
|
|
},
|
|
|
|
addTeamMembers: async (sessionId, members) => {
|
|
if (!members || members.length === 0) {
|
|
return { 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 [result] = await db.query(
|
|
`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 ?`,
|
|
[values]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
getTeamMembers: async (sessionId) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`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`,
|
|
[sessionId]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
removeTeamMember: async (sessionId, workerId) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`DELETE FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?`,
|
|
[sessionId, workerId]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
clearAllTeamMembers: async (sessionId) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`DELETE FROM tbm_team_assignments WHERE session_id = ?`,
|
|
[sessionId]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
// ==================== 안전 체크리스트 관련 ====================
|
|
|
|
getAllSafetyChecks: async () => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`SELECT * FROM tbm_safety_checks WHERE is_active = 1 ORDER BY check_category, display_order`
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
getSafetyChecksByCategory: async (category) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`SELECT * FROM tbm_safety_checks WHERE check_category = ? AND is_active = 1 ORDER BY display_order`,
|
|
[category]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
getSafetyRecords: async (sessionId) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`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 sso_users u ON sr.checked_by = u.user_id
|
|
WHERE sr.session_id = ?
|
|
ORDER BY sc.check_category, sc.display_order`,
|
|
[sessionId]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
saveSafetyRecord: async (recordData) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`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()`,
|
|
[recordData.session_id, recordData.check_id, recordData.is_checked, recordData.notes, recordData.checked_by]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
saveSafetyRecords: async (sessionId, records, checkedBy) => {
|
|
if (!records || records.length === 0) {
|
|
return { affectedRows: 0 };
|
|
}
|
|
|
|
const db = await getDb();
|
|
const values = records.map(r => [
|
|
sessionId,
|
|
r.check_id,
|
|
r.is_checked,
|
|
r.notes || null,
|
|
checkedBy
|
|
]);
|
|
|
|
const [result] = await db.query(
|
|
`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()`,
|
|
[values]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
// ==================== 작업 인계 관련 ====================
|
|
|
|
createHandover: async (handoverData) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`INSERT INTO team_handovers
|
|
(session_id, from_leader_id, to_leader_id, handover_date, handover_time,
|
|
reason, handover_notes, worker_ids)
|
|
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 || [])
|
|
]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
confirmHandover: async (handoverId, confirmedBy) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`UPDATE team_handovers
|
|
SET is_confirmed = 1, confirmed_at = NOW(), confirmed_by = ?
|
|
WHERE handover_id = ?`,
|
|
[confirmedBy, handoverId]
|
|
);
|
|
return result;
|
|
},
|
|
|
|
getHandoversByDate: async (date) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`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 sso_users u ON h.confirmed_by = u.user_id
|
|
WHERE h.handover_date = ?
|
|
ORDER BY h.handover_time DESC`,
|
|
[date]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
getPendingHandovers: async (toLeaderId) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`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`,
|
|
[toLeaderId]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
// ==================== 통계 및 리포트 ====================
|
|
|
|
getTbmStatistics: async (startDate, endDate) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`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`,
|
|
[startDate, endDate]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
getLeaderStatistics: async (startDate, endDate) => {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`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`,
|
|
[startDate, endDate]
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
getIncompleteWorkReports: async (userId) => {
|
|
const db = await getDb();
|
|
|
|
let whereClause = `
|
|
WHERE dwr.id IS NULL
|
|
AND s.status = 'completed'
|
|
AND (ta.attendance_type IS NULL OR ta.attendance_type != 'annual')
|
|
AND ta.task_id IS NOT NULL
|
|
AND ta.workplace_id IS NOT NULL
|
|
`;
|
|
const params = [];
|
|
|
|
if (userId !== null && userId !== undefined) {
|
|
whereClause = `
|
|
WHERE s.created_by = ?
|
|
AND dwr.id IS NULL
|
|
AND s.status = 'completed'
|
|
AND (ta.attendance_type IS NULL OR ta.attendance_type != 'annual')
|
|
AND ta.task_id IS NOT NULL
|
|
AND ta.workplace_id IS NOT NULL
|
|
`;
|
|
params.push(userId);
|
|
}
|
|
|
|
const [rows] = await db.query(
|
|
`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,
|
|
ta.attendance_type,
|
|
ta.attendance_hours,
|
|
ta.work_hours,
|
|
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,
|
|
lw.worker_name as leader_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 sso_users creator ON s.created_by = creator.user_id
|
|
LEFT JOIN workers lw ON s.leader_id = lw.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 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`,
|
|
params
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
// ========== 안전 체크리스트 확장 메서드 ==========
|
|
|
|
getSafetyChecksByType: async (checkType, options = {}) => {
|
|
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);
|
|
return rows;
|
|
},
|
|
|
|
getSafetyChecksByWeather: async (conditions) => {
|
|
if (!conditions || conditions.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const db = await getDb();
|
|
const placeholders = conditions.map(() => '?').join(',');
|
|
const [rows] = await db.query(
|
|
`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`,
|
|
conditions
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
getSafetyChecksByTasks: async (taskIds) => {
|
|
if (!taskIds || taskIds.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const db = await getDb();
|
|
const placeholders = taskIds.map(() => '?').join(',');
|
|
const [rows] = await db.query(
|
|
`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`,
|
|
taskIds
|
|
);
|
|
return rows;
|
|
},
|
|
|
|
getFilteredSafetyChecks: async (sessionId, weatherConditions = []) => {
|
|
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
|
|
}));
|
|
};
|
|
|
|
return {
|
|
basic: mergeWithRecords(basicChecks),
|
|
weather: mergeWithRecords(weatherChecks),
|
|
task: mergeWithRecords(taskChecks),
|
|
totalCount: basicChecks.length + weatherChecks.length + taskChecks.length,
|
|
weatherConditions: weatherConditions
|
|
};
|
|
},
|
|
|
|
createSafetyCheck: async (checkData) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`INSERT INTO tbm_safety_checks
|
|
(check_category, check_type, weather_condition, task_id, check_item, description, is_required, display_order)
|
|
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
|
|
]
|
|
);
|
|
return { insertId: result.insertId };
|
|
},
|
|
|
|
updateSafetyCheck: async (checkId, checkData) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`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 = ?`,
|
|
[
|
|
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
|
|
]
|
|
);
|
|
return { affectedRows: result.affectedRows };
|
|
},
|
|
|
|
deleteSafetyCheck: async (checkId) => {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`UPDATE tbm_safety_checks SET is_active = 0 WHERE check_id = ?`,
|
|
[checkId]
|
|
);
|
|
return { affectedRows: result.affectedRows };
|
|
}
|
|
};
|
|
|
|
module.exports = TbmModel;
|