feat: 안전 코드 tksafety 이관 + 사용자 관리 정리 + UI Tailwind 전환

Phase 1: tksafety에 출입신청/체크리스트 API·웹 추가, tkfb 안전 코드 삭제
Phase 2: 사용자 관리 페이지 삭제, API 축소, 알림 수신자 tkuser 이관
Phase 3: tkuser 권한 페이지 정의 업데이트
Phase 4: 전체 34개 페이지 Tailwind CSS + tkfb-core.js 전환,
         미사용 CSS 20개·인프라 JS 10개·템플릿·컴포넌트 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-13 10:46:22 +09:00
parent 8373fe9e75
commit 9fda89a374
133 changed files with 5255 additions and 26181 deletions

View File

@@ -10,9 +10,6 @@ let canvasImage = null;
// 금일 TBM 작업자 데이터
let todayWorkers = [];
// 금일 출입 신청 데이터
let todayVisitors = [];
// ==================== 초기화 ====================
document.addEventListener('DOMContentLoaded', async () => {
@@ -175,8 +172,6 @@ async function loadTodayData() {
// TBM 작업자 데이터 로드
await loadTodayWorkers(today);
// 출입 신청 데이터 로드
await loadTodayVisitors(today);
}
async function loadTodayWorkers(date) {
@@ -212,43 +207,6 @@ async function loadTodayWorkers(date) {
}
}
async function loadTodayVisitors(date) {
try {
// 날짜 형식 확인 (YYYY-MM-DD)
const formattedDate = date.split('T')[0];
const response = await window.apiCall(`/workplace-visits/requests`, 'GET');
if (response && response.success) {
const requests = response.data || [];
// 금일 날짜와 승인된 요청 필터링
todayVisitors = requests.filter(req => {
// UTC 변환 없이 로컬 날짜로 비교
const visitDateObj = new Date(req.visit_date);
const visitYear = visitDateObj.getFullYear();
const visitMonth = String(visitDateObj.getMonth() + 1).padStart(2, '0');
const visitDay = String(visitDateObj.getDate()).padStart(2, '0');
const visitDate = `${visitYear}-${visitMonth}-${visitDay}`;
return visitDate === formattedDate &&
(req.status === 'approved' || req.status === 'training_completed');
}).map(req => ({
workplace_id: req.workplace_id,
visitor_company: req.visitor_company,
visitor_count: req.visitor_count,
visit_time: req.visit_time,
purpose_name: req.purpose_name,
status: req.status
}));
console.log('로드된 방문자:', todayVisitors);
}
} catch (error) {
console.error('출입 신청 데이터 로드 오류:', error);
}
}
// ==================== 지도 렌더링 ====================
function renderMap() {
@@ -260,19 +218,17 @@ function renderMap() {
// 모든 작업장 영역 표시
mapRegions.forEach(region => {
// 해당 작업장의 작업자/방문자 인원 계산
// 해당 작업장의 작업자 인원 계산
const workers = todayWorkers.filter(w => w.workplace_id === region.workplace_id);
const visitors = todayVisitors.filter(v => v.workplace_id === region.workplace_id);
const totalWorkerCount = workers.reduce((sum, w) => sum + (w.member_count || 0), 0);
const totalVisitorCount = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0);
// 영역 그리기
drawWorkplaceRegion(region, totalWorkerCount, totalVisitorCount);
drawWorkplaceRegion(region, totalWorkerCount);
});
}
function drawWorkplaceRegion(region, workerCount, visitorCount) {
function drawWorkplaceRegion(region, workerCount) {
// 사각형 좌표 변환
const x1 = (region.x_start / 100) * canvas.width;
const y1 = (region.y_start / 100) * canvas.height;
@@ -286,20 +242,12 @@ function drawWorkplaceRegion(region, workerCount, visitorCount) {
// 색상 결정
let fillColor, strokeColor;
const hasActivity = workerCount > 0 || visitorCount > 0;
const hasActivity = workerCount > 0;
if (workerCount > 0 && visitorCount > 0) {
// 둘 다 있음 - 초록
fillColor = 'rgba(34, 197, 94, 0.3)';
strokeColor = 'rgb(34, 197, 94)';
} else if (workerCount > 0) {
// 내부 작업자만 - 파란색
if (workerCount > 0) {
// 작업자 있음 - 파란
fillColor = 'rgba(59, 130, 246, 0.3)';
strokeColor = 'rgb(59, 130, 246)';
} else if (visitorCount > 0) {
// 외부 방문자만 - 보라색
fillColor = 'rgba(168, 85, 247, 0.3)';
strokeColor = 'rgb(168, 85, 247)';
} else {
// 인원 없음 - 회색 테두리만
fillColor = 'rgba(0, 0, 0, 0)'; // 투명
@@ -332,9 +280,8 @@ function drawWorkplaceRegion(region, workerCount, visitorCount) {
ctx.stroke();
// 텍스트
const totalCount = workerCount + visitorCount;
ctx.fillStyle = strokeColor;
ctx.fillText(totalCount.toString(), centerX, centerY);
ctx.fillText(workerCount.toString(), centerX, centerY);
ctx.restore();
} else {
// 인원이 없을 때는 작업장 이름만 표시
@@ -389,7 +336,6 @@ let currentModalWorkplace = null;
function showWorkplaceDetail(workplace) {
currentModalWorkplace = workplace;
const workers = todayWorkers.filter(w => w.workplace_id === workplace.workplace_id);
const visitors = todayVisitors.filter(v => v.workplace_id === workplace.workplace_id);
// 모달 제목
document.getElementById('modalWorkplaceName').textContent = workplace.workplace_name;
@@ -397,15 +343,16 @@ function showWorkplaceDetail(workplace) {
// 요약 카드 업데이트
const totalWorkers = workers.reduce((sum, w) => sum + (w.member_count || 0), 0);
const totalVisitors = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0);
document.getElementById('summaryWorkerCount').textContent = totalWorkers;
document.getElementById('summaryVisitorCount').textContent = totalVisitors;
const summaryVisitorEl = document.getElementById('summaryVisitorCount');
if (summaryVisitorEl) summaryVisitorEl.textContent = '0';
document.getElementById('summaryTaskCount').textContent = workers.length;
// 배지 업데이트
document.getElementById('workerCountBadge').textContent = totalWorkers;
document.getElementById('visitorCountBadge').textContent = totalVisitors;
const visitorBadgeEl = document.getElementById('visitorCountBadge');
if (visitorBadgeEl) visitorBadgeEl.textContent = '0';
// 현황 개요 탭 - 현재 작업 목록
renderCurrentTasks(workers);
@@ -416,9 +363,6 @@ function showWorkplaceDetail(workplace) {
// 작업자 탭
renderWorkersTab(workers);
// 방문자 탭
renderVisitorsTab(visitors);
// 상세 지도 초기화
initDetailMap(workplace);
@@ -529,33 +473,6 @@ function renderWorkersTab(workers) {
container.innerHTML = html;
}
// 방문자 탭 렌더링
function renderVisitorsTab(visitors) {
const container = document.getElementById('externalVisitorsList');
if (visitors.length === 0) {
container.innerHTML = '<p class="empty-message">금일 방문 예정 인원이 없습니다.</p>';
return;
}
let html = '';
visitors.forEach(visitor => {
const statusText = visitor.status === 'training_completed' ? '교육 완료' : '승인됨';
html += `
<div class="visitor-item">
<div class="visitor-item-header">
<p class="visitor-item-title">${escapeHtml(visitor.visitor_company)}</p>
<span class="visitor-item-badge">${parseInt(visitor.visitor_count) || 0}명 • ${statusText}</span>
</div>
<p class="visitor-item-detail">⏰ ${escapeHtml(visitor.visit_time)}</p>
<p class="visitor-item-detail">📋 ${escapeHtml(visitor.purpose_name)}</p>
</div>
`;
});
container.innerHTML = html;
}
// 상세 지도 초기화
async function initDetailMap(workplace) {