Files
tk-factory-services/system1-factory/api/models/tbmModel.js
Hyungi Ahn 7637be33f3 feat: TBM 모바일 시스템 + 작업 분할/이동 + 권한 통합
TBM 시스템:
- 4단계 워크플로우 (draft→세부편집→완료→작업보고)
- 모바일 전용 TBM 페이지 (tbm-mobile.html) + 3단계 생성 위자드
- 작업자 작업 분할 (work_hours + split_seq)
- 작업자 이동 보내기/빼오기 (tbm_transfers 테이블)
- 생성 시 중복 배정 방지 (당일 배정 현황 조회)
- 데스크탑 TBM 페이지 세부편집 기능 추가

작업보고서:
- 모바일 전용 작업보고서 페이지 (report-create-mobile.html)
- TBM에서 사전 등록된 work_hours 자동 반영

권한 시스템:
- tkuser user_page_permissions 테이블과 system1 페이지 접근 연동
- pageAccessRoutes를 userRoutes보다 먼저 등록 (라우트 우선순위 수정)
- TKUSER_DEFAULT_ACCESS 폴백 추가 (개인→부서→기본값 3단계)
- 권한 캐시키 갱신 (userPageAccess_v2)

기타:
- app-init.js 캐시 버스팅 (v=5)
- iOS Safari touch-action: manipulation 적용
- KST 타임존 날짜 버그 수정 (toISOString UTC 이슈)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 07:46:21 +09:00

1182 lines
36 KiB
JavaScript

// 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,
GROUP_CONCAT(DISTINCT w2.worker_name ORDER BY ta.assignment_id SEPARATOR ', ') as team_member_names,
-- 이동 수 (이 세션이 source 또는 dest인 이동 건수)
(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
`;
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,
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
`;
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);
}
},
/**
* TBM 세션 완료 처리 (근태 유형 포함)
* @param {number} sessionId - TBM 세션 ID
* @param {string} endTime - 종료 시간
* @param {Array} attendanceData - [{worker_id, attendance_type, attendance_hours}]
* @param {number} createdBy - 처리자 user_id
*/
completeSessionWithAttendance: async (sessionId, endTime, attendanceData, createdBy, callback) => {
let conn;
try {
const db = await getDb();
conn = await db.getConnection();
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.rollback();
conn.release();
return callback(null, { affectedRows: 0 });
}
const sessionDate = sessionRows[0].session_date;
// sessionDate를 YYYY-MM-DD 형식으로 변환
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. 연차(annual) 작업자 → 작업보고서 자동 생성 (project_id=13, 8h)
const annualWorkers = attendanceData.filter(a => a.attendance_type === 'annual');
for (const aw of annualWorkers) {
// 해당 작업자의 assignment_id 조회
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();
conn.release();
// 5. 연차 작업자 근태 동기화
for (const aw of annualWorkers) {
try {
const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(aw.worker_id, reportDate);
} catch (syncErr) {
console.error('근태 동기화 오류 (무시됨):', syncErr);
}
}
callback(null, { affectedRows: 1 });
} catch (err) {
if (conn) {
try { await conn.rollback(); } catch (e) {}
conn.release();
}
callback(err);
}
},
/**
* TBM 세션 삭제 (draft 상태만 가능)
*/
deleteSession: async (sessionId, callback) => {
try {
const db = await getDb();
// draft 상태인 세션만 삭제 허용
const [result] = await db.query(
`DELETE FROM tbm_sessions WHERE session_id = ? AND status = 'draft'`,
[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, 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)
`;
const values = [
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
];
const [result] = await db.query(sql, values);
callback(null, result);
} catch (err) {
callback(err);
}
},
/**
* 분할 항목 추가 (같은 세션+작업자에 split_seq 자동 증가)
*/
addSplitAssignment: async (assignmentData, callback) => {
try {
const db = await getDb();
// 현재 최대 split_seq 조회
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 sql = `
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)
`;
const [result] = await db.query(sql, [
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
]);
callback(null, { assignment_id: result.insertId, split_seq: nextSeq });
} 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 sso_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 sso_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 조건 동적 생성
// TBM 완료(근태 입력) 후에만 작업보고서 작성 가능
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 = [];
// userId가 있으면 created_by 조건 추가 (일반 사용자)
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 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,
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
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;