Files
tk-factory-services/tksafety/api/models/visitRequestModel.js
Hyungi Ahn 2fc4179052 fix(tksafety): DB 스키마 불일치로 인한 API 500 에러 수정
- departments.name → department_name (3곳)
- users → sso_users 테이블 참조 수정 (7곳)
- tbm_sessions.start_time → created_at (존재하지 않는 컬럼)
- tbm_team_assignments JOIN: ta.user_id → ta.worker_id
- workers leader JOIN: leader.worker_id → leader.user_id
- tbm_weather_conditions → weather_conditions 테이블명 수정

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:51:06 +09:00

590 lines
20 KiB
JavaScript

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
};