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 += `
-
-
- ${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.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.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 => `