From 6a20056e056f664c7f9b4eea27590ea5eaed1d17 Mon Sep 17 00:00:00 2001
From: Hyungi Ahn
Date: Fri, 13 Mar 2026 14:24:13 +0900
Subject: [PATCH] =?UTF-8?q?feat(tksafety):=20=ED=86=B5=ED=95=A9=20?=
=?UTF-8?q?=EC=B6=9C=EC=9E=85=EC=8B=A0=EA=B3=A0=20=EA=B4=80=EB=A6=AC=20?=
=?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- DB 마이그레이션: request_type, visitor_name, department_id, check_in/out_time 컬럼 + status ENUM 확장
- 4소스 UNION 대시보드: 방문(외부/내부) + TBM + 협력업체 통합 조회
- 체크인/체크아웃 API + 내부 출입 신고(승인 불필요) 지원
- 통합 출입 현황판 페이지 신규 (entry-dashboard.html)
- 출입 신청/관리 페이지에 유형 필터 + 체크인/아웃 버튼 추가
- safety_entry_dashboard 권한 추가
Co-Authored-By: Claude Opus 4.6
---
.../api/controllers/visitRequestController.js | 90 ++++++-
tksafety/api/index.js | 9 +-
tksafety/api/models/visitRequestModel.js | 247 +++++++++++++++++-
tksafety/api/routes/visitRequestRoutes.js | 11 +
tksafety/web/checklist.html | 2 +-
tksafety/web/education.html | 2 +-
tksafety/web/entry-dashboard.html | 110 ++++++++
tksafety/web/index.html | 2 +-
tksafety/web/static/js/tksafety-core.js | 1 +
.../web/static/js/tksafety-entry-dashboard.js | 136 ++++++++++
.../static/js/tksafety-visit-management.js | 78 +++++-
.../web/static/js/tksafety-visit-request.js | 104 +++++++-
tksafety/web/training.html | 2 +-
tksafety/web/visit-management.html | 28 +-
tksafety/web/visit-request.html | 44 +++-
user-management/api/models/permissionModel.js | 1 +
16 files changed, 810 insertions(+), 57 deletions(-)
create mode 100644 tksafety/web/entry-dashboard.html
create mode 100644 tksafety/web/static/js/tksafety-entry-dashboard.js
diff --git a/tksafety/api/controllers/visitRequestController.js b/tksafety/api/controllers/visitRequestController.js
index 389811d..91f8ce3 100644
--- a/tksafety/api/controllers/visitRequestController.js
+++ b/tksafety/api/controllers/visitRequestController.js
@@ -6,18 +6,31 @@ exports.createVisitRequest = async (req, res) => {
try {
const requester_id = req.user.user_id;
const requestData = { requester_id, ...req.body };
+ const isInternal = requestData.request_type === 'internal';
- const requiredFields = ['visitor_company', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
- for (const field of requiredFields) {
- if (!requestData[field]) {
- return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
+ // 내부 출입: visitor_name 필수, 외부: visitor_company 필수
+ if (isInternal) {
+ const requiredFields = ['visitor_name', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
+ for (const field of requiredFields) {
+ if (!requestData[field]) {
+ return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
+ }
+ }
+ requestData.visitor_count = 1;
+ requestData.visitor_company = requestData.visitor_company || '내부';
+ } else {
+ const requiredFields = ['visitor_company', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
+ for (const field of requiredFields) {
+ if (!requestData[field]) {
+ return res.status(400).json({ success: false, message: `${field}는 필수 입력 항목입니다.` });
+ }
}
}
const requestId = await visitRequestModel.createVisitRequest(requestData);
res.status(201).json({
success: true,
- message: '출입 신청이 성공적으로 생성되었습니다.',
+ message: isInternal ? '내부 출입 신고가 완료되었습니다.' : '출입 신청이 성공적으로 생성되었습니다.',
data: { request_id: requestId }
});
} catch (err) {
@@ -34,7 +47,8 @@ exports.getAllVisitRequests = async (req, res) => {
start_date: req.query.start_date,
end_date: req.query.end_date,
requester_id: req.query.requester_id,
- category_id: req.query.category_id
+ category_id: req.query.category_id,
+ request_type: req.query.request_type
};
const requests = await visitRequestModel.getAllVisitRequests(filters);
@@ -303,3 +317,67 @@ exports.getWorkplaces = async (req, res) => {
res.status(500).json({ success: false, message: '작업장 조회 중 오류가 발생했습니다.' });
}
};
+
+// ==================== 체크인/체크아웃 ====================
+
+exports.checkIn = async (req, res) => {
+ try {
+ const result = await visitRequestModel.checkIn(req.params.id, req.user.user_id);
+ if (result.error) {
+ return res.status(result.status).json({ success: false, message: result.error });
+ }
+ res.json({ success: true, message: '체크인되었습니다.' });
+ } catch (err) {
+ console.error('체크인 오류:', err);
+ res.status(500).json({ success: false, message: '체크인 중 오류가 발생했습니다.' });
+ }
+};
+
+exports.checkOut = async (req, res) => {
+ try {
+ const result = await visitRequestModel.checkOut(req.params.id, req.user.user_id);
+ if (result.error) {
+ return res.status(result.status).json({ success: false, message: result.error });
+ }
+ res.json({ success: true, message: '체크아웃되었습니다.' });
+ } catch (err) {
+ console.error('체크아웃 오류:', err);
+ res.status(500).json({ success: false, message: '체크아웃 중 오류가 발생했습니다.' });
+ }
+};
+
+// ==================== 통합 대시보드 ====================
+
+exports.getEntryDashboard = async (req, res) => {
+ try {
+ const date = req.query.date || new Date().toISOString().substring(0, 10);
+ const data = await visitRequestModel.getEntryDashboard(date);
+ res.json({ success: true, data, date });
+ } catch (err) {
+ console.error('출입 대시보드 조회 오류:', err);
+ res.status(500).json({ success: false, message: '출입 대시보드 조회 중 오류가 발생했습니다.' });
+ }
+};
+
+exports.getEntryStats = async (req, res) => {
+ try {
+ const date = req.query.date || new Date().toISOString().substring(0, 10);
+ const stats = await visitRequestModel.getEntryStats(date);
+ res.json({ success: true, data: stats, date });
+ } catch (err) {
+ console.error('출입 통계 조회 오류:', err);
+ res.status(500).json({ success: false, message: '출입 통계 조회 중 오류가 발생했습니다.' });
+ }
+};
+
+// ==================== 부서 목록 ====================
+
+exports.getDepartments = async (req, res) => {
+ try {
+ const departments = await visitRequestModel.getAllDepartments();
+ res.json({ success: true, data: departments });
+ } catch (err) {
+ console.error('부서 목록 조회 오류:', err);
+ res.status(500).json({ success: false, message: '부서 목록 조회 중 오류가 발생했습니다.' });
+ }
+};
diff --git a/tksafety/api/index.js b/tksafety/api/index.js
index 34a6b02..36178fc 100644
--- a/tksafety/api/index.js
+++ b/tksafety/api/index.js
@@ -6,6 +6,7 @@ const educationRoutes = require('./routes/educationRoutes');
const visitRequestRoutes = require('./routes/visitRequestRoutes');
const checklistRoutes = require('./routes/checklistRoutes');
const dailyVisitModel = require('./models/dailyVisitModel');
+const visitRequestModel = require('./models/visitRequestModel');
const { requireAuth } = require('./middleware/auth');
const app = express();
@@ -83,8 +84,14 @@ cron.schedule('59 23 * * *', async () => {
}
}, { timezone: 'Asia/Seoul' });
-app.listen(PORT, () => {
+app.listen(PORT, async () => {
console.log(`tksafety-api running on port ${PORT}`);
+ // DB 마이그레이션 실행
+ try {
+ await visitRequestModel.runMigration();
+ } catch (err) {
+ console.error('Migration error:', err.message);
+ }
});
module.exports = app;
diff --git a/tksafety/api/models/visitRequestModel.js b/tksafety/api/models/visitRequestModel.js
index 31f651a..1eb3caf 100644
--- a/tksafety/api/models/visitRequestModel.js
+++ b/tksafety/api/models/visitRequestModel.js
@@ -1,13 +1,63 @@
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,
@@ -16,13 +66,16 @@ const createVisitRequest = async (requestData) => {
notes = null
} = requestData;
+ // 내부 출입은 바로 approved
+ const status = request_type === 'internal' ? 'approved' : 'pending';
+
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]
+ (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;
@@ -32,21 +85,26 @@ const getAllVisitRequests = async (filters = {}) => {
const db = getPool();
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.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
+ approver.username as approver_name,
+ d.name as department_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
+ LEFT JOIN departments d ON vr.department_id = d.department_id
WHERE 1=1
`;
@@ -72,6 +130,10 @@ const getAllVisitRequests = async (filters = {}) => {
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`;
@@ -83,21 +145,26 @@ const getVisitRequestById = async (requestId) => {
const db = getPool();
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.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
+ approver.username as approver_name,
+ d.name as department_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
+ LEFT JOIN departments d ON vr.department_id = d.department_id
WHERE vr.request_id = ?`,
[requestId]
);
@@ -342,7 +409,158 @@ const getWorkplacesByCategory = async (categoryId) => {
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 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, ts.start_time as entry_time,
+ ts.start_time 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.user_id = wk.worker_id
+ LEFT JOIN workers leader ON ts.leader_user_id = leader.worker_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, name FROM departments WHERE is_active = 1 ORDER BY name'
+ );
+ return rows;
+};
+
module.exports = {
+ runMigration,
createVisitRequest,
getAllVisitRequests,
getVisitRequestById,
@@ -351,6 +569,10 @@ module.exports = {
approveVisitRequest,
rejectVisitRequest,
updateVisitRequestStatus,
+ checkIn,
+ checkOut,
+ getEntryDashboard,
+ getEntryStats,
getAllVisitPurposes,
getActiveVisitPurposes,
createVisitPurpose,
@@ -362,5 +584,6 @@ module.exports = {
completeTraining,
getTrainingRecords,
getAllCategories,
- getWorkplacesByCategory
+ getWorkplacesByCategory,
+ getAllDepartments
};
diff --git a/tksafety/api/routes/visitRequestRoutes.js b/tksafety/api/routes/visitRequestRoutes.js
index fef1250..e02d6e2 100644
--- a/tksafety/api/routes/visitRequestRoutes.js
+++ b/tksafety/api/routes/visitRequestRoutes.js
@@ -14,10 +14,21 @@ router.delete('/requests/:id', requirePage('safety_visit_request'), visitRequest
router.put('/requests/:id/approve', requireAdmin, visitRequestController.approveVisitRequest);
router.put('/requests/:id/reject', requireAdmin, visitRequestController.rejectVisitRequest);
+// Check-in / Check-out
+router.put('/requests/:id/check-in', visitRequestController.checkIn);
+router.put('/requests/:id/check-out', visitRequestController.checkOut);
+
+// Entry Dashboard (통합 출입 현황)
+router.get('/entry-dashboard', visitRequestController.getEntryDashboard);
+router.get('/entry-dashboard/stats', visitRequestController.getEntryStats);
+
// Categories & Workplaces
router.get('/categories', visitRequestController.getAllCategories);
router.get('/workplaces', visitRequestController.getWorkplaces);
+// Departments
+router.get('/departments', visitRequestController.getDepartments);
+
// Visit purposes
router.get('/purposes', visitRequestController.getAllVisitPurposes);
router.get('/purposes/active', visitRequestController.getActiveVisitPurposes);
diff --git a/tksafety/web/checklist.html b/tksafety/web/checklist.html
index a580194..8a2a28d 100644
--- a/tksafety/web/checklist.html
+++ b/tksafety/web/checklist.html
@@ -154,7 +154,7 @@
-
+
+
+
+
+
+
+
+
TK 안전관리
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | 소스 |
+ 이름 |
+ 소속 |
+ 작업장 |
+ 입장시간 |
+ 퇴장시간 |
+ 목적 |
+ 상태 |
+ 비고 |
+
+
+
+ | 로딩 중... |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tksafety/web/education.html b/tksafety/web/education.html
index 00dd228..0107f77 100644
--- a/tksafety/web/education.html
+++ b/tksafety/web/education.html
@@ -187,7 +187,7 @@
-
+
diff --git a/tksafety/web/entry-dashboard.html b/tksafety/web/entry-dashboard.html
new file mode 100644
index 0000000..9182c94
--- /dev/null
+++ b/tksafety/web/entry-dashboard.html
@@ -0,0 +1,110 @@
+
+
+