- 실시간 작업장 현황을 지도로 시각화 - 작업장 관리 페이지에서 정의한 구역 정보 활용 - TBM 작업자 및 방문자 현황 표시 주요 변경사항: - dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거) - workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현 - modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가 시각화 방식: - 인원 없음: 회색 테두리 + 작업장 이름 - 내부 작업자: 파란색 영역 + 인원 수 - 외부 방문자: 보라색 영역 + 인원 수 - 둘 다: 초록색 영역 + 총 인원 수 기술 구현: - Canvas API 기반 사각형 영역 렌더링 - map-regions API를 통한 데이터 일관성 보장 - 클릭 이벤트로 상세 정보 모달 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
506 lines
13 KiB
JavaScript
506 lines
13 KiB
JavaScript
const { getDb } = require('../dbPool');
|
|
|
|
// ==================== 출입 신청 관리 ====================
|
|
|
|
/**
|
|
* 출입 신청 생성
|
|
*/
|
|
const createVisitRequest = async (requestData, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const {
|
|
requester_id,
|
|
visitor_company,
|
|
visitor_count = 1,
|
|
category_id,
|
|
workplace_id,
|
|
visit_date,
|
|
visit_time,
|
|
purpose_id,
|
|
notes = null
|
|
} = requestData;
|
|
|
|
const [result] = await db.query(
|
|
`INSERT INTO workplace_visit_requests
|
|
(requester_id, visitor_company, visitor_count, category_id, workplace_id,
|
|
visit_date, visit_time, purpose_id, notes)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[requester_id, visitor_company, visitor_count, category_id, workplace_id,
|
|
visit_date, visit_time, purpose_id, notes]
|
|
);
|
|
|
|
callback(null, result.insertId);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 출입 신청 목록 조회 (필터 옵션 포함)
|
|
*/
|
|
const getAllVisitRequests = async (filters = {}, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
let query = `
|
|
SELECT
|
|
vr.request_id, vr.requester_id, vr.visitor_company, vr.visitor_count,
|
|
vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time,
|
|
vr.purpose_id, vr.notes, vr.status,
|
|
vr.approved_by, vr.approved_at, vr.rejection_reason,
|
|
vr.created_at, vr.updated_at,
|
|
u.username as requester_name, u.name as requester_full_name,
|
|
wc.category_name, w.workplace_name,
|
|
vpt.purpose_name,
|
|
approver.username as approver_name
|
|
FROM workplace_visit_requests vr
|
|
INNER JOIN users u ON vr.requester_id = u.user_id
|
|
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
|
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
|
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
|
LEFT JOIN users approver ON vr.approved_by = approver.user_id
|
|
WHERE 1=1
|
|
`;
|
|
|
|
const params = [];
|
|
|
|
// 필터 적용
|
|
if (filters.status) {
|
|
query += ` AND vr.status = ?`;
|
|
params.push(filters.status);
|
|
}
|
|
|
|
if (filters.visit_date) {
|
|
query += ` AND vr.visit_date = ?`;
|
|
params.push(filters.visit_date);
|
|
}
|
|
|
|
if (filters.start_date && filters.end_date) {
|
|
query += ` AND vr.visit_date BETWEEN ? AND ?`;
|
|
params.push(filters.start_date, filters.end_date);
|
|
}
|
|
|
|
if (filters.requester_id) {
|
|
query += ` AND vr.requester_id = ?`;
|
|
params.push(filters.requester_id);
|
|
}
|
|
|
|
if (filters.category_id) {
|
|
query += ` AND vr.category_id = ?`;
|
|
params.push(filters.category_id);
|
|
}
|
|
|
|
query += ` ORDER BY vr.visit_date DESC, vr.visit_time DESC, vr.created_at DESC`;
|
|
|
|
const [rows] = await db.query(query, params);
|
|
callback(null, rows);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 출입 신청 상세 조회
|
|
*/
|
|
const getVisitRequestById = async (requestId, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`SELECT
|
|
vr.request_id, vr.requester_id, vr.visitor_company, vr.visitor_count,
|
|
vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time,
|
|
vr.purpose_id, vr.notes, vr.status,
|
|
vr.approved_by, vr.approved_at, vr.rejection_reason,
|
|
vr.created_at, vr.updated_at,
|
|
u.username as requester_name, u.name as requester_full_name,
|
|
wc.category_name, w.workplace_name,
|
|
vpt.purpose_name,
|
|
approver.username as approver_name
|
|
FROM workplace_visit_requests vr
|
|
INNER JOIN users u ON vr.requester_id = u.user_id
|
|
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
|
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
|
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
|
LEFT JOIN users approver ON vr.approved_by = approver.user_id
|
|
WHERE vr.request_id = ?`,
|
|
[requestId]
|
|
);
|
|
|
|
callback(null, rows[0]);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 출입 신청 수정
|
|
*/
|
|
const updateVisitRequest = async (requestId, requestData, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const {
|
|
visitor_company,
|
|
visitor_count,
|
|
category_id,
|
|
workplace_id,
|
|
visit_date,
|
|
visit_time,
|
|
purpose_id,
|
|
notes
|
|
} = requestData;
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET visitor_company = ?, visitor_count = ?, category_id = ?, workplace_id = ?,
|
|
visit_date = ?, visit_time = ?, purpose_id = ?, notes = ?, updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[visitor_company, visitor_count, category_id, workplace_id,
|
|
visit_date, visit_time, purpose_id, notes, requestId]
|
|
);
|
|
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 출입 신청 삭제
|
|
*/
|
|
const deleteVisitRequest = async (requestId, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`DELETE FROM workplace_visit_requests WHERE request_id = ?`,
|
|
[requestId]
|
|
);
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 출입 신청 승인
|
|
*/
|
|
const approveVisitRequest = async (requestId, approvedBy, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET status = 'approved', approved_by = ?, approved_at = NOW(), updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[approvedBy, requestId]
|
|
);
|
|
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 출입 신청 반려
|
|
*/
|
|
const rejectVisitRequest = async (requestId, rejectionData, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const { approved_by, rejection_reason } = rejectionData;
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET status = 'rejected', approved_by = ?, approved_at = NOW(),
|
|
rejection_reason = ?, updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[approved_by, rejection_reason, requestId]
|
|
);
|
|
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 출입 신청 상태 변경
|
|
*/
|
|
const updateVisitRequestStatus = async (requestId, status, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`UPDATE workplace_visit_requests
|
|
SET status = ?, updated_at = NOW()
|
|
WHERE request_id = ?`,
|
|
[status, requestId]
|
|
);
|
|
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
// ==================== 방문 목적 관리 ====================
|
|
|
|
/**
|
|
* 모든 방문 목적 조회
|
|
*/
|
|
const getAllVisitPurposes = async (callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
|
|
FROM visit_purpose_types
|
|
ORDER BY display_order ASC, purpose_id ASC`
|
|
);
|
|
callback(null, rows);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 활성 방문 목적만 조회
|
|
*/
|
|
const getActiveVisitPurposes = async (callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
|
|
FROM visit_purpose_types
|
|
WHERE is_active = TRUE
|
|
ORDER BY display_order ASC, purpose_id ASC`
|
|
);
|
|
callback(null, rows);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 방문 목적 추가
|
|
*/
|
|
const createVisitPurpose = async (purposeData, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const { purpose_name, display_order = 0, is_active = true } = purposeData;
|
|
|
|
const [result] = await db.query(
|
|
`INSERT INTO visit_purpose_types (purpose_name, display_order, is_active)
|
|
VALUES (?, ?, ?)`,
|
|
[purpose_name, display_order, is_active]
|
|
);
|
|
|
|
callback(null, result.insertId);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 방문 목적 수정
|
|
*/
|
|
const updateVisitPurpose = async (purposeId, purposeData, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const { purpose_name, display_order, is_active } = purposeData;
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE visit_purpose_types
|
|
SET purpose_name = ?, display_order = ?, is_active = ?
|
|
WHERE purpose_id = ?`,
|
|
[purpose_name, display_order, is_active, purposeId]
|
|
);
|
|
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 방문 목적 삭제
|
|
*/
|
|
const deleteVisitPurpose = async (purposeId, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`DELETE FROM visit_purpose_types WHERE purpose_id = ?`,
|
|
[purposeId]
|
|
);
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
// ==================== 안전교육 기록 관리 ====================
|
|
|
|
/**
|
|
* 안전교육 기록 생성
|
|
*/
|
|
const createTrainingRecord = async (trainingData, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const {
|
|
request_id,
|
|
trainer_id,
|
|
training_date,
|
|
training_start_time,
|
|
training_end_time = null,
|
|
training_topics = null
|
|
} = trainingData;
|
|
|
|
const [result] = await db.query(
|
|
`INSERT INTO safety_training_records
|
|
(request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
[request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics]
|
|
);
|
|
|
|
callback(null, result.insertId);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 특정 출입 신청의 안전교육 기록 조회
|
|
*/
|
|
const getTrainingRecordByRequestId = async (requestId, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [rows] = await db.query(
|
|
`SELECT
|
|
str.training_id, str.request_id, str.trainer_id, str.training_date,
|
|
str.training_start_time, str.training_end_time, str.training_topics,
|
|
str.signature_data, str.completed_at, str.created_at, str.updated_at,
|
|
u.username as trainer_name, u.name as trainer_full_name
|
|
FROM safety_training_records str
|
|
INNER JOIN users u ON str.trainer_id = u.user_id
|
|
WHERE str.request_id = ?`,
|
|
[requestId]
|
|
);
|
|
|
|
callback(null, rows[0]);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 안전교육 기록 수정
|
|
*/
|
|
const updateTrainingRecord = async (trainingId, trainingData, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const {
|
|
training_date,
|
|
training_start_time,
|
|
training_end_time,
|
|
training_topics
|
|
} = trainingData;
|
|
|
|
const [result] = await db.query(
|
|
`UPDATE safety_training_records
|
|
SET training_date = ?, training_start_time = ?, training_end_time = ?,
|
|
training_topics = ?, updated_at = NOW()
|
|
WHERE training_id = ?`,
|
|
[training_date, training_start_time, training_end_time, training_topics, trainingId]
|
|
);
|
|
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 안전교육 완료 (서명 포함)
|
|
*/
|
|
const completeTraining = async (trainingId, signatureData, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
const [result] = await db.query(
|
|
`UPDATE safety_training_records
|
|
SET signature_data = ?, completed_at = NOW(), updated_at = NOW()
|
|
WHERE training_id = ?`,
|
|
[signatureData, trainingId]
|
|
);
|
|
|
|
callback(null, result);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 안전교육 목록 조회 (날짜별 필터)
|
|
*/
|
|
const getTrainingRecords = async (filters = {}, callback) => {
|
|
try {
|
|
const db = await getDb();
|
|
let query = `
|
|
SELECT
|
|
str.training_id, str.request_id, str.trainer_id, str.training_date,
|
|
str.training_start_time, str.training_end_time, str.training_topics,
|
|
str.completed_at, str.created_at, str.updated_at,
|
|
u.username as trainer_name, u.name as trainer_full_name,
|
|
vr.visitor_company, vr.visitor_count, vr.visit_date
|
|
FROM safety_training_records str
|
|
INNER JOIN users u ON str.trainer_id = u.user_id
|
|
INNER JOIN workplace_visit_requests vr ON str.request_id = vr.request_id
|
|
WHERE 1=1
|
|
`;
|
|
|
|
const params = [];
|
|
|
|
if (filters.training_date) {
|
|
query += ` AND str.training_date = ?`;
|
|
params.push(filters.training_date);
|
|
}
|
|
|
|
if (filters.start_date && filters.end_date) {
|
|
query += ` AND str.training_date BETWEEN ? AND ?`;
|
|
params.push(filters.start_date, filters.end_date);
|
|
}
|
|
|
|
if (filters.trainer_id) {
|
|
query += ` AND str.trainer_id = ?`;
|
|
params.push(filters.trainer_id);
|
|
}
|
|
|
|
query += ` ORDER BY str.training_date DESC, str.training_start_time DESC`;
|
|
|
|
const [rows] = await db.query(query, params);
|
|
callback(null, rows);
|
|
} catch (err) {
|
|
callback(err);
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
// 출입 신청
|
|
createVisitRequest,
|
|
getAllVisitRequests,
|
|
getVisitRequestById,
|
|
updateVisitRequest,
|
|
deleteVisitRequest,
|
|
approveVisitRequest,
|
|
rejectVisitRequest,
|
|
updateVisitRequestStatus,
|
|
|
|
// 방문 목적
|
|
getAllVisitPurposes,
|
|
getActiveVisitPurposes,
|
|
createVisitPurpose,
|
|
updateVisitPurpose,
|
|
deleteVisitPurpose,
|
|
|
|
// 안전교육
|
|
createTrainingRecord,
|
|
getTrainingRecordByRequestId,
|
|
updateTrainingRecord,
|
|
completeTraining,
|
|
getTrainingRecords
|
|
};
|