diff --git a/docs/README.md b/docs/README.md index c69c967..d053c2b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,7 @@ TK-FB-Project/ | 1 | [CODING_GUIDE.md](../CODING_GUIDE.md) | 프로젝트 실행, 코딩 규칙, API 개발 | 모든 개발자 | | 2 | [DEV_LOG.md](../DEV_LOG.md) | 최근 개발 현황 및 변경사항 | 모든 개발자 | | 3 | [guides/SETUP.md](guides/SETUP.md) | 개발 환경 상세 설정 | 신규 개발자 | +| 4 | [SECURITY_GUIDE.md](SECURITY_GUIDE.md) | 보안 취약점 및 개발 가이드 | 모든 개발자 | --- @@ -55,6 +56,7 @@ TK-FB-Project/ | [guides/SETUP.md](guides/SETUP.md) | 개발 환경 설정 | | [guides/work-report-time-input-guide.md](guides/work-report-time-input-guide.md) | 작업보고서 시간 입력 UX | | [TBM_DEPLOYMENT_GUIDE.md](TBM_DEPLOYMENT_GUIDE.md) | TBM 시스템 배포/사용 가이드 | +| [SECURITY_GUIDE.md](SECURITY_GUIDE.md) | **보안 가이드 (필독)** - 취약점 분석 및 보안 개발 가이드 | ### 3. 아키텍처 문서 (Architecture) diff --git a/web-ui/components/sidebar-nav.html b/web-ui/components/sidebar-nav.html index c85b827..a214682 100644 --- a/web-ui/components/sidebar-nav.html +++ b/web-ui/components/sidebar-nav.html @@ -136,9 +136,6 @@ 설비 관리 - - 코드 관리 - 신고 카테고리 관리 diff --git a/web-ui/css/daily-patrol.css b/web-ui/css/daily-patrol.css index d912771..6bd6b10 100644 --- a/web-ui/css/daily-patrol.css +++ b/web-ui/css/daily-patrol.css @@ -1,79 +1,141 @@ /* daily-patrol.css - 일일순회점검 페이지 스타일 */ -/* 세션 선택 영역 */ -.patrol-session-selector { +/* 점검 시작 영역 */ +.patrol-start-section { display: flex; - flex-wrap: wrap; - gap: 1.5rem; - padding: 1.5rem; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2rem; + padding: 3rem 1.5rem; background: var(--surface-color, #fff); border-radius: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); margin-bottom: 1.5rem; } -.patrol-date-time { +.patrol-start-section .btn-lg { + padding: 1.25rem 3rem; + font-size: 1.25rem; + border-radius: 12px; display: flex; - flex-wrap: wrap; - align-items: flex-end; - gap: 1rem; + align-items: center; + gap: 0.75rem; } -.patrol-date-time .form-group { - min-width: 140px; +.patrol-start-section .btn-icon { + font-size: 1.5rem; } -.patrol-time-buttons { - display: flex; - gap: 0.5rem; -} - -.patrol-time-btn { - padding: 0.5rem 1.25rem; - border: 2px solid var(--border-color, #e2e8f0); +/* 공장 선택 영역 */ +.factory-selection-area { + padding: 2rem; background: var(--surface-color, #fff); - border-radius: 8px; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 1.5rem; +} + +.factory-selection-header { + text-align: center; + margin-bottom: 2rem; +} + +.factory-selection-header h3 { + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 0.5rem; +} + +.factory-selection-subtitle { + color: var(--text-secondary, #64748b); + margin: 0; +} + +.factory-cards-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + max-width: 800px; + margin: 0 auto; +} + +.factory-card { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem 1.5rem; + background: var(--bg-color, #f8fafc); + border: 2px solid var(--border-color, #e2e8f0); + border-radius: 16px; cursor: pointer; - font-weight: 500; transition: all 0.2s; } -.patrol-time-btn:hover { +.factory-card:hover { border-color: var(--primary-color, #3b82f6); + background: #eff6ff; + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(59, 130, 246, 0.15); } -.patrol-time-btn.active { - background: var(--primary-color, #3b82f6); - border-color: var(--primary-color, #3b82f6); - color: #fff; +.factory-card:active { + transform: translateY(-2px); +} + +.factory-card-icon { + width: 80px; + height: 80px; + border-radius: 12px; + background: var(--surface-color, #fff); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1rem; + overflow: hidden; + font-size: 2.5rem; +} + +.factory-card-icon img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.factory-card-name { + font-size: 1.125rem; + font-weight: 600; + text-align: center; } /* 오늘 점검 현황 요약 */ .today-status-summary { - flex: 1; - min-width: 300px; display: flex; - gap: 1rem; + gap: 1.5rem; align-items: center; - padding: 1rem; + justify-content: center; + padding: 1rem 2rem; background: var(--bg-color, #f8fafc); - border-radius: 8px; + border-radius: 12px; } .status-card { text-align: center; - padding: 0.75rem 1rem; + padding: 1rem 1.5rem; + background: var(--surface-color, #fff); + border-radius: 8px; + min-width: 100px; } .status-label { - font-size: 0.8rem; + font-size: 0.85rem; color: var(--text-secondary, #64748b); - margin-bottom: 0.25rem; + margin-bottom: 0.5rem; } .status-value { - font-size: 1.25rem; - font-weight: 600; + font-size: 1.5rem; + font-weight: 700; } .status-value.completed { @@ -84,6 +146,12 @@ color: var(--warning-color, #f59e0b); } +.status-sub { + font-size: 0.75rem; + color: var(--text-secondary, #64748b); + margin-top: 0.25rem; +} + /* 점검 영역 */ .patrol-area { background: var(--surface-color, #fff); @@ -265,12 +333,15 @@ z-index: 10; } -/* 작업장 목록 (지도 대체) */ +/* 작업장 목록 */ .workplace-list-container { display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.75rem; margin-top: 1rem; + padding: 0.5rem; + background: var(--bg-color, #f8fafc); + border-radius: 8px; } .workplace-card { @@ -292,11 +363,21 @@ background: #f0fdf4; } -.workplace-card.selected { +.workplace-card.in-progress { border-color: var(--primary-color, #3b82f6); background: #eff6ff; } +.workplace-card.selected { + border-color: var(--primary-color, #3b82f6); + background: var(--primary-color, #3b82f6); + color: #fff; +} + +.workplace-card.selected .workplace-card-status { + color: rgba(255, 255, 255, 0.8); +} + .workplace-card-name { font-weight: 600; margin-bottom: 0.25rem; @@ -586,17 +667,47 @@ /* 반응형 */ @media (max-width: 768px) { - .patrol-session-selector { - flex-direction: column; + .patrol-start-section { + padding: 2rem 1rem; } - .patrol-date-time { - flex-direction: column; + .patrol-start-section .btn-lg { + width: 100%; + justify-content: center; + } + + .today-status-summary { + flex-direction: row; width: 100%; } - .patrol-date-time .form-group { - width: 100%; + .status-card { + flex: 1; + min-width: auto; + padding: 0.75rem; + } + + .factory-selection-area { + padding: 1.5rem 1rem; + } + + .factory-cards-container { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } + + .factory-card { + padding: 1.5rem 1rem; + } + + .factory-card-icon { + width: 60px; + height: 60px; + font-size: 2rem; + } + + .factory-card-name { + font-size: 1rem; } .patrol-content { diff --git a/web-ui/js/code-management.js b/web-ui/js/code-management.js deleted file mode 100644 index 55c1f77..0000000 --- a/web-ui/js/code-management.js +++ /dev/null @@ -1,766 +0,0 @@ -// 코드 관리 페이지 JavaScript - -// 전역 변수 -let workStatusTypes = []; -let errorTypes = []; -let workTypes = []; -let currentCodeType = 'work-status'; -let currentEditingCode = null; - -// 페이지 초기화 -document.addEventListener('DOMContentLoaded', function() { - console.log('🏷️ 코드 관리 페이지 초기화 시작'); - - initializePage(); - loadAllCodes(); -}); - -// 페이지 초기화 -function initializePage() { - // 시간 업데이트 시작 - updateCurrentTime(); - setInterval(updateCurrentTime, 1000); - - // 사용자 정보 업데이트 - updateUserInfo(); - - // 프로필 메뉴 토글 - setupProfileMenu(); - - // 로그아웃 버튼 - setupLogoutButton(); -} - -// 현재 시간 업데이트 -function updateCurrentTime() { - const now = new Date(); - const timeString = now.toLocaleTimeString('ko-KR', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - - const timeElement = document.getElementById('timeValue'); - if (timeElement) { - timeElement.textContent = timeString; - } -} - -// 사용자 정보 업데이트 -// navbar/sidebar는 app-init.js에서 공통 처리 -function updateUserInfo() { - // app-init.js가 navbar 사용자 정보를 처리 -} - -// 프로필 메뉴 설정 -function setupProfileMenu() { - const userProfile = document.getElementById('userProfile'); - const profileMenu = document.getElementById('profileMenu'); - - if (userProfile && profileMenu) { - userProfile.addEventListener('click', function(e) { - e.stopPropagation(); - const isVisible = profileMenu.style.display === 'block'; - profileMenu.style.display = isVisible ? 'none' : 'block'; - }); - - // 외부 클릭 시 메뉴 닫기 - document.addEventListener('click', function() { - profileMenu.style.display = 'none'; - }); - } -} - -// 로그아웃 버튼 설정 -function setupLogoutButton() { - const logoutBtn = document.getElementById('logoutBtn'); - if (logoutBtn) { - logoutBtn.addEventListener('click', function() { - if (confirm('로그아웃 하시겠습니까?')) { - localStorage.removeItem('token'); - localStorage.removeItem('user'); - localStorage.removeItem('userInfo'); - window.location.href = '/index.html'; - } - }); - } -} - -// 모든 코드 데이터 로드 -async function loadAllCodes() { - try { - console.log('📊 모든 코드 데이터 로딩 시작'); - - await Promise.all([ - loadWorkStatusTypes(), - // loadErrorTypes(), // 오류 유형은 신고 시스템으로 대체됨 - loadWorkTypes() - ]); - - // 현재 활성 탭 렌더링 - renderCurrentTab(); - - } catch (error) { - console.error('코드 데이터 로딩 오류:', error); - showToast('코드 데이터를 불러오는데 실패했습니다.', 'error'); - } -} - -// 작업 상태 유형 로드 -async function loadWorkStatusTypes() { - try { - console.log('📊 작업 상태 유형 로딩...'); - - const response = await apiCall('/daily-work-reports/work-status-types', 'GET'); - - let statusData = []; - if (response && response.success && Array.isArray(response.data)) { - statusData = response.data; - } else if (Array.isArray(response)) { - statusData = response; - } - - workStatusTypes = statusData; - console.log(`✅ 작업 상태 유형 ${workStatusTypes.length}개 로드 완료`); - - } catch (error) { - console.error('작업 상태 유형 로딩 오류:', error); - workStatusTypes = []; - } -} - -// 오류 유형 로드 -async function loadErrorTypes() { - try { - console.log('⚠️ 오류 유형 로딩...'); - - const response = await apiCall('/daily-work-reports/error-types', 'GET'); - - let errorData = []; - if (response && response.success && Array.isArray(response.data)) { - errorData = response.data; - } else if (Array.isArray(response)) { - errorData = response; - } - - errorTypes = errorData; - console.log(`✅ 오류 유형 ${errorTypes.length}개 로드 완료`); - - } catch (error) { - console.error('오류 유형 로딩 오류:', error); - errorTypes = []; - } -} - -// 작업 유형 로드 -async function loadWorkTypes() { - try { - console.log('🔧 작업 유형 로딩...'); - - const response = await apiCall('/daily-work-reports/work-types', 'GET'); - - let typeData = []; - if (response && response.success && Array.isArray(response.data)) { - typeData = response.data; - } else if (Array.isArray(response)) { - typeData = response; - } - - workTypes = typeData; - console.log(`✅ 작업 유형 ${workTypes.length}개 로드 완료`); - - } catch (error) { - console.error('작업 유형 로딩 오류:', error); - workTypes = []; - } -} - -// 코드 탭 전환 -function switchCodeTab(tabName) { - // 탭 버튼 활성화 상태 변경 - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.classList.remove('active'); - }); - document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); - - // 탭 콘텐츠 표시/숨김 - document.querySelectorAll('.code-tab-content').forEach(content => { - content.classList.remove('active'); - }); - document.getElementById(`${tabName}-tab`).classList.add('active'); - - currentCodeType = tabName; - renderCurrentTab(); -} - -// 현재 탭 렌더링 -function renderCurrentTab() { - switch (currentCodeType) { - case 'work-status': - renderWorkStatusTypes(); - break; - // case 'error-types': // 오류 유형은 신고 시스템으로 대체됨 - // renderErrorTypes(); - // break; - case 'work-types': - renderWorkTypes(); - break; - } -} - -// 작업 상태 유형 렌더링 -function renderWorkStatusTypes() { - const grid = document.getElementById('workStatusGrid'); - if (!grid) return; - - if (workStatusTypes.length === 0) { - grid.innerHTML = ` -
-
📊
-

등록된 작업 상태 유형이 없습니다.

-

"새 상태 추가" 버튼을 눌러 작업 상태를 등록해보세요.

- -
- `; - updateWorkStatusStats(); - return; - } - - let gridHtml = ''; - - workStatusTypes.forEach(status => { - const isError = status.is_error === 1 || status.is_error === true; - const statusClass = isError ? 'error-status' : 'normal-status'; - const statusIcon = isError ? '❌' : '✅'; - const statusLabel = isError ? '오류' : '정상'; - - gridHtml += ` -
-
-
${statusIcon}
-
-

${status.name}

- ${statusLabel} -
-
- - -
-
- ${status.description ? `

${status.description}

` : ''} -
- 등록: ${formatDate(status.created_at)} -
-
- `; - }); - - grid.innerHTML = gridHtml; - updateWorkStatusStats(); -} - -// 오류 유형 렌더링 -function renderErrorTypes() { - const grid = document.getElementById('errorTypesGrid'); - if (!grid) return; - - if (errorTypes.length === 0) { - grid.innerHTML = ` -
-
⚠️
-

등록된 오류 유형이 없습니다.

-

"새 오류 유형 추가" 버튼을 눌러 오류 유형을 등록해보세요.

- -
- `; - updateErrorTypesStats(); - return; - } - - let gridHtml = ''; - - errorTypes.forEach(error => { - const severityMap = { - 'low': { icon: '🟢', label: '낮음', class: 'severity-low' }, - 'medium': { icon: '🟡', label: '보통', class: 'severity-medium' }, - 'high': { icon: '🟠', label: '높음', class: 'severity-high' }, - 'critical': { icon: '🔴', label: '심각', class: 'severity-critical' } - }; - - const severity = severityMap[error.severity] || severityMap.medium; - - gridHtml += ` -
-
-
⚠️
-
-

${error.name}

- ${severity.icon} ${severity.label} -
-
- - -
-
- ${error.description ? `

${error.description}

` : ''} - ${error.solution_guide ? `
해결 가이드:
${error.solution_guide}
` : ''} -
- 등록: ${formatDate(error.created_at)} - ${error.updated_at !== error.created_at ? `수정: ${formatDate(error.updated_at)}` : ''} -
-
- `; - }); - - grid.innerHTML = gridHtml; - updateErrorTypesStats(); -} - -// 작업 유형 렌더링 -function renderWorkTypes() { - const grid = document.getElementById('workTypesGrid'); - if (!grid) return; - - if (workTypes.length === 0) { - grid.innerHTML = ` -
-
🔧
-

등록된 작업 유형이 없습니다.

-

"새 작업 유형 추가" 버튼을 눌러 작업 유형을 등록해보세요.

- -
- `; - updateWorkTypesStats(); - return; - } - - let gridHtml = ''; - - workTypes.forEach(type => { - gridHtml += ` -
-
-
🔧
-
-

${type.name}

- ${type.category ? `📁 ${type.category}` : ''} -
-
- - -
-
- ${type.description ? `

${type.description}

` : ''} -
- 등록: ${formatDate(type.created_at)} - ${type.updated_at !== type.created_at ? `수정: ${formatDate(type.updated_at)}` : ''} -
-
- `; - }); - - grid.innerHTML = gridHtml; - updateWorkTypesStats(); -} - -// 작업 상태 통계 업데이트 -function updateWorkStatusStats() { - const total = workStatusTypes.length; - const normal = workStatusTypes.filter(s => !s.is_error).length; - const error = workStatusTypes.filter(s => s.is_error).length; - - document.getElementById('workStatusCount').textContent = total; - document.getElementById('normalStatusCount').textContent = normal; - document.getElementById('errorStatusCount').textContent = error; -} - -// 오류 유형 통계 업데이트 -function updateErrorTypesStats() { - const total = errorTypes.length; - const critical = errorTypes.filter(e => e.severity === 'critical').length; - const high = errorTypes.filter(e => e.severity === 'high').length; - const medium = errorTypes.filter(e => e.severity === 'medium').length; - const low = errorTypes.filter(e => e.severity === 'low').length; - - document.getElementById('errorTypesCount').textContent = total; - document.getElementById('criticalErrorsCount').textContent = critical; - document.getElementById('highErrorsCount').textContent = high; - document.getElementById('mediumErrorsCount').textContent = medium; - document.getElementById('lowErrorsCount').textContent = low; -} - -// 작업 유형 통계 업데이트 -function updateWorkTypesStats() { - const total = workTypes.length; - const categories = new Set(workTypes.map(t => t.category).filter(Boolean)).size; - - document.getElementById('workTypesCount').textContent = total; - document.getElementById('workCategoriesCount').textContent = categories; -} - -// 코드 모달 열기 -function openCodeModal(codeType, codeData = null) { - const modal = document.getElementById('codeModal'); - const modalTitle = document.getElementById('modalTitle'); - const deleteBtn = document.getElementById('deleteCodeBtn'); - - if (!modal) return; - - currentEditingCode = codeData; - - // 모든 전용 필드 숨기기 - document.getElementById('isErrorGroup').style.display = 'none'; - document.getElementById('severityGroup').style.display = 'none'; - document.getElementById('solutionGuideGroup').style.display = 'none'; - document.getElementById('categoryGroup').style.display = 'none'; - - // 코드 유형별 설정 - switch (codeType) { - case 'work-status': - modalTitle.textContent = codeData ? '작업 상태 수정' : '새 작업 상태 추가'; - document.getElementById('isErrorGroup').style.display = 'block'; - break; - case 'error-types': - modalTitle.textContent = codeData ? '오류 유형 수정' : '새 오류 유형 추가'; - document.getElementById('severityGroup').style.display = 'block'; - document.getElementById('solutionGuideGroup').style.display = 'block'; - break; - case 'work-types': - modalTitle.textContent = codeData ? '작업 유형 수정' : '새 작업 유형 추가'; - document.getElementById('categoryGroup').style.display = 'block'; - updateCategoryList(); - break; - } - - document.getElementById('codeType').value = codeType; - - if (codeData) { - // 수정 모드 - deleteBtn.style.display = 'inline-flex'; - - // 폼에 데이터 채우기 - document.getElementById('codeId').value = codeData.id; - document.getElementById('codeName').value = codeData.name || ''; - document.getElementById('codeDescription').value = codeData.description || ''; - - // 코드 유형별 필드 채우기 - if (codeType === 'work-status') { - document.getElementById('isError').checked = codeData.is_error === 1 || codeData.is_error === true; - } else if (codeType === 'error-types') { - document.getElementById('severity').value = codeData.severity || 'medium'; - document.getElementById('solutionGuide').value = codeData.solution_guide || ''; - } else if (codeType === 'work-types') { - document.getElementById('category').value = codeData.category || ''; - } - } else { - // 신규 등록 모드 - deleteBtn.style.display = 'none'; - - // 폼 초기화 - document.getElementById('codeForm').reset(); - document.getElementById('codeId').value = ''; - document.getElementById('codeType').value = codeType; - } - - modal.style.display = 'flex'; - document.body.style.overflow = 'hidden'; - - // 첫 번째 입력 필드에 포커스 - setTimeout(() => { - document.getElementById('codeName').focus(); - }, 100); -} - -// 카테고리 목록 업데이트 -function updateCategoryList() { - const categoryList = document.getElementById('categoryList'); - if (categoryList) { - const categories = [...new Set(workTypes.map(t => t.category).filter(Boolean))].sort(); - categoryList.innerHTML = categories.map(cat => `