const { getPool } = require('../middleware/auth'); // ==================== DB 마이그레이션 ==================== const runMigration = async () => { const db = getPool(); try { // 컬럼 존재 여부 체크 const [cols] = await db.query( `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'workplace_visit_requests' AND COLUMN_NAME IN ('request_type','visitor_name','department_id','check_in_time','check_out_time')` ); const existing = cols.map(c => c.COLUMN_NAME); if (!existing.includes('request_type')) { await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN request_type ENUM('external','internal') NOT NULL DEFAULT 'external' AFTER request_id`); console.log('[migration] Added request_type column'); } if (!existing.includes('visitor_name')) { await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN visitor_name VARCHAR(100) NULL AFTER visitor_company`); console.log('[migration] Added visitor_name column'); } if (!existing.includes('department_id')) { await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN department_id INT NULL AFTER visitor_name`); console.log('[migration] Added department_id column'); } if (!existing.includes('check_in_time')) { await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN check_in_time DATETIME NULL AFTER status`); console.log('[migration] Added check_in_time column'); } if (!existing.includes('check_out_time')) { await db.query(`ALTER TABLE workplace_visit_requests ADD COLUMN check_out_time DATETIME NULL AFTER check_in_time`); console.log('[migration] Added check_out_time column'); } // status ENUM 확장 (checked_in, checked_out 추가) await db.query(`ALTER TABLE workplace_visit_requests MODIFY COLUMN status ENUM('pending','approved','rejected','training_completed','checked_in','checked_out') NOT NULL DEFAULT 'pending'`); // 인덱스 추가 (이미 있으면 무시) try { await db.query(`CREATE INDEX idx_visit_date_type ON workplace_visit_requests (visit_date, request_type)`); } catch (e) { /* already exists */ } try { await db.query(`CREATE INDEX idx_checkin_time ON workplace_visit_requests (check_in_time)`); } catch (e) { /* already exists */ } console.log('[migration] workplace_visit_requests migration complete'); } catch (err) { console.error('[migration] Error:', err.message); } }; // ==================== 출입 신청 관리 ==================== const createVisitRequest = async (requestData) => { const db = getPool(); const { requester_id, request_type = 'external', visitor_company, visitor_name = null, visitor_count = 1, department_id = null, category_id, workplace_id, visit_date, visit_time, purpose_id, notes = null } = requestData; // 내부 출입은 바로 approved const status = request_type === 'internal' ? 'approved' : 'pending'; const [result] = await db.query( `INSERT INTO workplace_visit_requests (requester_id, request_type, visitor_company, visitor_name, visitor_count, department_id, category_id, workplace_id, visit_date, visit_time, purpose_id, notes, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [requester_id, request_type, visitor_company, visitor_name, visitor_count, department_id, category_id, workplace_id, visit_date, visit_time, purpose_id, notes, status] ); return result.insertId; }; const getAllVisitRequests = async (filters = {}) => { const db = getPool(); let query = ` SELECT vr.request_id, vr.requester_id, vr.request_type, vr.visitor_company, vr.visitor_name, vr.visitor_count, vr.department_id, vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time, vr.purpose_id, vr.notes, vr.status, vr.check_in_time, vr.check_out_time, 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, d.department_name FROM workplace_visit_requests vr INNER JOIN sso_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 sso_users approver ON vr.approved_by = approver.user_id LEFT JOIN departments d ON vr.department_id = d.department_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); } if (filters.request_type) { query += ` AND vr.request_type = ?`; params.push(filters.request_type); } query += ` ORDER BY vr.visit_date DESC, vr.visit_time DESC, vr.created_at DESC`; const [rows] = await db.query(query, params); return rows; }; const getVisitRequestById = async (requestId) => { const db = getPool(); const [rows] = await db.query( `SELECT vr.request_id, vr.requester_id, vr.request_type, vr.visitor_company, vr.visitor_name, vr.visitor_count, vr.department_id, vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time, vr.purpose_id, vr.notes, vr.status, vr.check_in_time, vr.check_out_time, 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, d.department_name FROM workplace_visit_requests vr INNER JOIN sso_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 sso_users approver ON vr.approved_by = approver.user_id LEFT JOIN departments d ON vr.department_id = d.department_id WHERE vr.request_id = ?`, [requestId] ); return rows[0]; }; const updateVisitRequest = async (requestId, requestData) => { const db = getPool(); 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] ); return result; }; const deleteVisitRequest = async (requestId) => { const db = getPool(); const [result] = await db.query( `DELETE FROM workplace_visit_requests WHERE request_id = ?`, [requestId] ); return result; }; const approveVisitRequest = async (requestId, approvedBy) => { const db = getPool(); 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] ); return result; }; const rejectVisitRequest = async (requestId, rejectionData) => { const db = getPool(); 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] ); return result; }; const updateVisitRequestStatus = async (requestId, status) => { const db = getPool(); const [result] = await db.query( `UPDATE workplace_visit_requests SET status = ?, updated_at = NOW() WHERE request_id = ?`, [status, requestId] ); return result; }; // ==================== 방문 목적 관리 ==================== const getAllVisitPurposes = async () => { const db = getPool(); 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` ); return rows; }; const getActiveVisitPurposes = async () => { const db = getPool(); 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` ); return rows; }; const createVisitPurpose = async (purposeData) => { const db = getPool(); 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] ); return result.insertId; }; const updateVisitPurpose = async (purposeId, purposeData) => { const db = getPool(); 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] ); return result; }; const deleteVisitPurpose = async (purposeId) => { const db = getPool(); const [result] = await db.query( `DELETE FROM visit_purpose_types WHERE purpose_id = ?`, [purposeId] ); return result; }; // ==================== 안전교육 기록 관리 ==================== const createTrainingRecord = async (trainingData) => { const db = getPool(); 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] ); return result.insertId; }; const getTrainingRecordByRequestId = async (requestId) => { const db = getPool(); 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 sso_users u ON str.trainer_id = u.user_id WHERE str.request_id = ?`, [requestId] ); return rows[0]; }; const updateTrainingRecord = async (trainingId, trainingData) => { const db = getPool(); 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] ); return result; }; const completeTraining = async (trainingId, signatureData) => { const db = getPool(); const [result] = await db.query( `UPDATE safety_training_records SET signature_data = ?, completed_at = NOW(), updated_at = NOW() WHERE training_id = ?`, [signatureData, trainingId] ); return result; }; const getTrainingRecords = async (filters = {}) => { const db = getPool(); 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 sso_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); return rows; }; // ==================== 작업장 분류/작업장 조회 ==================== const getAllCategories = async () => { const db = getPool(); const [rows] = await db.query( 'SELECT category_id, category_name FROM workplace_categories ORDER BY category_name' ); return rows; }; const getWorkplacesByCategory = async (categoryId) => { const db = getPool(); let query = 'SELECT workplace_id, workplace_name, category_id FROM workplaces'; const params = []; if (categoryId) { query += ' WHERE category_id = ?'; params.push(categoryId); } query += ' ORDER BY workplace_name'; const [rows] = await db.query(query, params); return rows; }; // ==================== 체크인/체크아웃 ==================== const checkIn = async (requestId, userId) => { const db = getPool(); // 승인 또는 교육완료 상태만 체크인 가능 const [rows] = await db.query( `SELECT request_id, requester_id, status, request_type FROM workplace_visit_requests WHERE request_id = ?`, [requestId] ); if (!rows.length) return { error: '신청을 찾을 수 없습니다.', status: 404 }; const req = rows[0]; const allowedStatuses = req.request_type === 'internal' ? ['approved'] : ['approved', 'training_completed']; if (!allowedStatuses.includes(req.status)) { return { error: `체크인 불가 상태입니다. (현재: ${req.status})`, status: 400 }; } const [result] = await db.query( `UPDATE workplace_visit_requests SET status = 'checked_in', check_in_time = NOW(), updated_at = NOW() WHERE request_id = ?`, [requestId] ); return { success: true, result }; }; const checkOut = async (requestId, userId) => { const db = getPool(); const [rows] = await db.query( `SELECT request_id, requester_id, status FROM workplace_visit_requests WHERE request_id = ?`, [requestId] ); if (!rows.length) return { error: '신청을 찾을 수 없습니다.', status: 404 }; if (rows[0].status !== 'checked_in') { return { error: `체크아웃 불가 상태입니다. (현재: ${rows[0].status})`, status: 400 }; } const [result] = await db.query( `UPDATE workplace_visit_requests SET status = 'checked_out', check_out_time = NOW(), updated_at = NOW() WHERE request_id = ?`, [requestId] ); return { success: true, result }; }; // ==================== 통합 대시보드 ==================== const getEntryDashboard = async (date) => { const db = getPool(); const query = ` SELECT 'visit' as source, vr.request_type, vr.visitor_company, vr.visitor_name, vr.visitor_count, wc.category_name, w.workplace_name, vr.visit_date as entry_date, vr.visit_time as entry_time, vr.check_in_time, vr.check_out_time, vr.status, u.name as reporter_name, vpt.purpose_name, NULL as source_note FROM workplace_visit_requests vr LEFT JOIN workplace_categories wc ON vr.category_id = wc.category_id LEFT JOIN workplaces w ON vr.workplace_id = w.workplace_id LEFT JOIN sso_users u ON vr.requester_id = u.user_id LEFT JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id WHERE vr.visit_date = ? AND vr.status NOT IN ('pending','rejected') UNION ALL SELECT 'tbm' as source, 'internal' as request_type, NULL as visitor_company, wk.worker_name as visitor_name, 1 as visitor_count, NULL as category_name, ts.work_location as workplace_name, ts.session_date as entry_date, TIME(ts.created_at) as entry_time, ts.created_at as check_in_time, ts.end_time as check_out_time, CASE WHEN ta.is_present=1 THEN 'checked_in' ELSE 'absent' END as status, leader.worker_name as reporter_name, '작업(TBM)' as purpose_name, 'TBM 세션 기준' as source_note FROM tbm_team_assignments ta JOIN tbm_sessions ts ON ta.session_id = ts.session_id JOIN workers wk ON ta.worker_id = wk.worker_id LEFT JOIN workers leader ON ts.leader_user_id = leader.user_id WHERE ts.session_date = ? AND ts.status != 'cancelled' UNION ALL SELECT 'partner' as source, 'external' as request_type, pc.company_name as visitor_company, pwc.worker_names as visitor_name, pwc.actual_worker_count as visitor_count, NULL as category_name, ps.workplace_name, DATE(pwc.check_in_time) as entry_date, TIME(pwc.check_in_time) as entry_time, pwc.check_in_time, pwc.check_out_time, CASE WHEN pwc.check_out_time IS NOT NULL THEN 'checked_out' ELSE 'checked_in' END as status, u2.name as reporter_name, ps.work_description as purpose_name, NULL as source_note FROM partner_work_checkins pwc JOIN partner_schedules ps ON pwc.schedule_id = ps.id JOIN partner_companies pc ON pwc.company_id = pc.id LEFT JOIN sso_users u2 ON pwc.checked_by = u2.user_id WHERE DATE(pwc.check_in_time) = ? ORDER BY entry_date DESC, check_in_time DESC `; const [rows] = await db.query(query, [date, date, date]); return rows; }; const getEntryStats = async (date) => { const db = getPool(); // 방문자 (visit source — request_type별 분리) const [visitStats] = await db.query( `SELECT request_type, SUM(visitor_count) as cnt FROM workplace_visit_requests WHERE visit_date = ? AND status = 'checked_in' GROUP BY request_type`, [date] ); // 협력업체 const [partnerStats] = await db.query( `SELECT COALESCE(SUM(actual_worker_count), 0) as cnt FROM partner_work_checkins WHERE DATE(check_in_time) = ? AND check_out_time IS NULL`, [date] ); // TBM (참고용) const [tbmStats] = await db.query( `SELECT COUNT(*) as cnt FROM tbm_team_assignments ta JOIN tbm_sessions ts ON ta.session_id = ts.session_id WHERE ts.session_date = ? AND ts.status != 'cancelled' AND ta.is_present = 1`, [date] ); const external = visitStats.find(v => v.request_type === 'external'); const internal = visitStats.find(v => v.request_type === 'internal'); return { external_visit: Number(external?.cnt || 0), internal_visit: Number(internal?.cnt || 0), partner: Number(partnerStats[0]?.cnt || 0), tbm: Number(tbmStats[0]?.cnt || 0) }; }; // ==================== 부서 목록 조회 ==================== const getAllDepartments = async () => { const db = getPool(); const [rows] = await db.query( 'SELECT department_id, department_name FROM departments WHERE is_active = 1 ORDER BY department_name' ); return rows; }; module.exports = { runMigration, createVisitRequest, getAllVisitRequests, getVisitRequestById, updateVisitRequest, deleteVisitRequest, approveVisitRequest, rejectVisitRequest, updateVisitRequestStatus, checkIn, checkOut, getEntryDashboard, getEntryStats, getAllVisitPurposes, getActiveVisitPurposes, createVisitPurpose, updateVisitPurpose, deleteVisitPurpose, createTrainingRecord, getTrainingRecordByRequestId, updateTrainingRecord, completeTraining, getTrainingRecords, getAllCategories, getWorkplacesByCategory, getAllDepartments };