From 234a6252c0aede9b1f6b5459dbc1ecdb0dd9a5a2 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Thu, 12 Feb 2026 15:52:45 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=EC=8B=A0?= =?UTF-8?q?=EA=B3=A0=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=EC=B6=95=20?= =?UTF-8?q?+=20tkqc=20=EC=97=B0=EB=8F=99=20+=20tkuser=20=EC=9D=B4=EC=8A=88?= =?UTF-8?q?=EC=9C=A0=ED=98=95=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tkreport 모바일 신고 페이지 (5단계 위자드: 유형→위치→프로젝트→항목→사진) - 프로젝트 DB 연동 (아코디언 UI: TBM등록/활성프로젝트/모름) - 클라이언트 이미지 리사이징 (1280px, JPEG 80%) - nginx client_max_body_size 50m, /api/projects/ 프록시 추가 - 부적합 신고 → tkqc 자동 연동 (사진 base64 전달, SSO 토큰 유지) - work_issue_reports에 project_id 컬럼 추가 - imageUploadService 경로 수정 (public/uploads → uploads, Docker 볼륨 일치) - tkuser 이슈유형 탭, 휴가관리, nginx 프록시 업데이트 - tkqc 대시보드/수신함/관리함/폐기함 UI 업데이트 - system1 랜딩페이지 업데이트 Co-Authored-By: Claude Opus 4.6 --- .gitignore | 5 + system1-factory/web/index.html | 20 +- .../api/controllers/workIssueController.js | 36 +- system2-report/api/models/workIssueModel.js | 7 +- .../api/services/imageUploadService.js | 4 +- .../api/services/mProjectService.js | 13 +- system2-report/web/js/issue-report.js | 479 ++++--- system2-report/web/nginx.conf | 15 +- .../web/pages/safety/issue-report.html | 1108 +++++++---------- .../web/pages/safety/report-status.html | 4 +- .../web/issues-archive.html | 34 +- .../web/issues-dashboard.html | 88 +- system3-nonconformance/web/issues-inbox.html | 26 +- .../web/issues-management.html | 34 +- .../web/static/js/core/page-manager.js | 14 +- user-management/api/models/vacationModel.js | 6 +- user-management/web/index.html | 613 +++++---- user-management/web/nginx.conf | 10 + 18 files changed, 1308 insertions(+), 1208 deletions(-) diff --git a/.gitignore b/.gitignore index c1c3d5a..82d85fc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ __pycache__/ uploads/ venv/ .DS_Store +._* +*.DS_Store +coverage/ +db_archive/ +*.log diff --git a/system1-factory/web/index.html b/system1-factory/web/index.html index db273e0..96a1ad0 100644 --- a/system1-factory/web/index.html +++ b/system1-factory/web/index.html @@ -6,25 +6,7 @@ (주)테크니컬코리아 생산팀 포털 diff --git a/system2-report/api/controllers/workIssueController.js b/system2-report/api/controllers/workIssueController.js index c090a11..04cb962 100644 --- a/system2-report/api/controllers/workIssueController.js +++ b/system2-report/api/controllers/workIssueController.js @@ -208,6 +208,7 @@ exports.createReport = async (req, res) => { issue_category_id, issue_item_id, custom_item_name, // 직접 입력한 항목명 + project_id, additional_description, photos = [] } = req.body; @@ -275,6 +276,7 @@ exports.createReport = async (req, res) => { reporter_id, factory_category_id: factory_category_id || null, workplace_id: workplace_id || null, + project_id: project_id || null, custom_location: custom_location || null, tbm_session_id: tbm_session_id || null, visit_request_id: visit_request_id || null, @@ -306,23 +308,35 @@ exports.createReport = async (req, res) => { }); if (categoryInfo && categoryInfo.category_type === 'nonconformity') { - // 사진은 System 2에만 저장, URL 참조만 전달 - const baseUrl = process.env.SYSTEM2_PUBLIC_URL || 'https://tkreport.technicalkorea.net'; - const photoUrls = Object.values(photoPaths).filter(Boolean) - .map(p => `${baseUrl}/api/uploads/${p}`); - - const descParts = [additional_description || categoryInfo.category_name]; - if (photoUrls.length > 0) { - descParts.push('', '[첨부 사진]'); - photoUrls.forEach((url, i) => descParts.push(`${i + 1}. ${url}`)); + // 저장된 사진 파일을 base64로 읽어서 System 3에 전달 + const fs = require('fs').promises; + const path = require('path'); + const photoBase64List = []; + for (const p of Object.values(photoPaths)) { + if (!p) continue; + try { + const filePath = path.join(__dirname, '..', p); + const buf = await fs.readFile(filePath); + const b64 = `data:image/jpeg;base64,${buf.toString('base64')}`; + photoBase64List.push(b64); + } catch (readErr) { + console.error('사진 파일 읽기 실패:', p, readErr.message); + } } + const descText = additional_description || categoryInfo.category_name; + + // 원래 신고자의 SSO 토큰 추출 + const originalToken = (req.headers['authorization'] || '').replace('Bearer ', ''); + const result = await mProjectService.sendToMProject({ category: categoryInfo.category_name, - description: descParts.join('\n'), + description: descText, reporter_name: req.user.name || req.user.username, tk_issue_id: reportId, - photos: [] // 사진 복사 안 함 (URL 참조만) + project_id: project_id || null, + photos: photoBase64List, + ssoToken: originalToken }); if (result.success && result.mProjectId) { workIssueModel.updateMProjectId(reportId, result.mProjectId, () => {}); diff --git a/system2-report/api/models/workIssueModel.js b/system2-report/api/models/workIssueModel.js index 4351f0f..4633c66 100644 --- a/system2-report/api/models/workIssueModel.js +++ b/system2-report/api/models/workIssueModel.js @@ -231,6 +231,7 @@ const createReport = async (reportData, callback) => { reporter_id, factory_category_id = null, workplace_id = null, + project_id = null, custom_location = null, tbm_session_id = null, visit_request_id = null, @@ -249,11 +250,11 @@ const createReport = async (reportData, callback) => { const [result] = await db.query( `INSERT INTO work_issue_reports - (reporter_id, report_date, factory_category_id, workplace_id, custom_location, + (reporter_id, report_date, factory_category_id, workplace_id, project_id, custom_location, tbm_session_id, visit_request_id, issue_category_id, issue_item_id, additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [reporter_id, reportDate, factory_category_id, workplace_id, custom_location, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [reporter_id, reportDate, factory_category_id, workplace_id, project_id, custom_location, tbm_session_id, visit_request_id, issue_category_id, issue_item_id, additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5] ); diff --git a/system2-report/api/services/imageUploadService.js b/system2-report/api/services/imageUploadService.js index 7870130..358b94c 100644 --- a/system2-report/api/services/imageUploadService.js +++ b/system2-report/api/services/imageUploadService.js @@ -21,8 +21,8 @@ try { // 업로드 디렉토리 설정 const UPLOAD_DIRS = { - issues: path.join(__dirname, '../public/uploads/issues'), - equipments: path.join(__dirname, '../public/uploads/equipments') + issues: path.join(__dirname, '../uploads/issues'), + equipments: path.join(__dirname, '../uploads/equipments') }; const UPLOAD_DIR = UPLOAD_DIRS.issues; // 기존 호환성 유지 const MAX_SIZE = { width: 1920, height: 1920 }; diff --git a/system2-report/api/services/mProjectService.js b/system2-report/api/services/mProjectService.js index de67917..2a8bc40 100644 --- a/system2-report/api/services/mProjectService.js +++ b/system2-report/api/services/mProjectService.js @@ -184,14 +184,21 @@ async function sendToMProject(issueData) { description, reporter_name, project_name, + project_id = null, tk_issue_id, photos = [], + ssoToken = null, } = issueData; logger.info('M-Project 연동 시작', { tk_issue_id, category }); - // 인증 토큰 획득 - const token = await getAuthToken(); + // SSO 토큰이 있으면 원래 사용자로 전송, 없으면 api_service 토큰 + let token; + if (ssoToken) { + token = ssoToken; + } else { + token = await getAuthToken(); + } if (!token) { return { success: false, error: 'M-Project 인증 실패' }; } @@ -219,7 +226,7 @@ async function sendToMProject(issueData) { const requestBody = { category: mProjectCategory, description: enhancedDescription, - project_id: M_PROJECT_CONFIG.defaultProjectId, + project_id: project_id || M_PROJECT_CONFIG.defaultProjectId, }; // 사진 추가 diff --git a/system2-report/web/js/issue-report.js b/system2-report/web/js/issue-report.js index 383edc1..ba8b281 100644 --- a/system2-report/web/js/issue-report.js +++ b/system2-report/web/js/issue-report.js @@ -1,23 +1,26 @@ /** * 신고 등록 페이지 JavaScript - * URL 파라미터 ?type=nonconformity 또는 ?type=safety로 유형 사전 선택 지원 + * 흐름: 유형(1) → 위치(2) → 프로젝트(3) → 카테고리/항목(4) → 사진/상세(5) → 제출 + * URL 파라미터 ?type=nonconformity|safety|facility 로 유형 사전 선택 지원 */ // API 설정 -const API_BASE = window.API_BASE_URL || 'http://localhost:30005/api'; +const API_BASE = window.API_BASE_URL || 'http://localhost:30105/api'; // 상태 변수 let selectedFactoryId = null; let selectedWorkplaceId = null; let selectedWorkplaceName = null; -let selectedType = null; // 'nonconformity' | 'safety' +let selectedType = null; // 'nonconformity' | 'safety' | 'facility' let selectedCategoryId = null; let selectedCategoryName = null; let selectedItemId = null; +let selectedProjectId = null; // 선택된 프로젝트 ID let selectedTbmSessionId = null; let selectedVisitRequestId = null; +let projectSelected = false; // 프로젝트 선택 완료 여부 let photos = [null, null, null, null, null]; -let customItemName = null; // 직접 입력한 항목명 +let customItemName = null; // 지도 관련 변수 let canvas, ctx, canvasImage; @@ -25,6 +28,9 @@ let mapRegions = []; let todayWorkers = []; let todayVisitors = []; +// 프로젝트 관련 변수 +let allProjects = []; // 전체 활성 프로젝트 목록 + // DOM 요소 let factorySelect, issueMapCanvas; let photoInput, currentPhotoIndex; @@ -38,16 +44,16 @@ document.addEventListener('DOMContentLoaded', async () => { canvas = issueMapCanvas; ctx = canvas.getContext('2d'); - // 이벤트 리스너 설정 setupEventListeners(); - // 공장 목록 로드 + // 프로젝트 먼저 로드 후 공장 로드 (공장 로드 시 renderProjectList 호출하므로) + await loadProjects(); await loadFactories(); // URL 파라미터에서 유형 확인 및 자동 선택 const urlParams = new URLSearchParams(window.location.search); const preselectedType = urlParams.get('type'); - if (preselectedType === 'nonconformity' || preselectedType === 'safety') { + if (['nonconformity', 'safety', 'facility'].includes(preselectedType)) { onTypeSelect(preselectedType); } }); @@ -56,24 +62,20 @@ document.addEventListener('DOMContentLoaded', async () => { * 이벤트 리스너 설정 */ function setupEventListeners() { - // 공장 선택 factorySelect.addEventListener('change', onFactoryChange); - - // 지도 클릭 canvas.addEventListener('click', onMapClick); // 기타 위치 토글 document.getElementById('useCustomLocation').addEventListener('change', (e) => { const customInput = document.getElementById('customLocationInput'); customInput.classList.toggle('visible', e.target.checked); - if (e.target.checked) { - // 지도 선택 초기화 selectedWorkplaceId = null; selectedWorkplaceName = null; selectedTbmSessionId = null; selectedVisitRequestId = null; updateLocationInfo(); + renderProjectList(); // 기타 위치에서도 프로젝트 목록 표시 } }); @@ -105,6 +107,27 @@ function setupEventListeners() { photoInput.addEventListener('change', onPhotoSelect); } +/** + * 활성 프로젝트 목록 로드 + */ +async function loadProjects() { + try { + const response = await fetch(`${API_BASE}/projects/active/list`, { + headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` } + }); + + if (!response.ok) throw new Error('프로젝트 목록 조회 실패'); + + const data = await response.json(); + if (data.success && data.data) { + allProjects = data.data; + } + } catch (error) { + console.error('프로젝트 목록 로드 실패:', error); + allProjects = []; + } +} + /** * 공장 목록 로드 */ @@ -125,7 +148,6 @@ async function loadFactories() { factorySelect.appendChild(option); }); - // 첫 번째 공장 자동 선택 if (data.data.length > 0) { factorySelect.value = data.data[0].category_id; onFactoryChange(); @@ -143,14 +165,14 @@ async function onFactoryChange() { selectedFactoryId = factorySelect.value; if (!selectedFactoryId) return; - // 위치 선택 초기화 selectedWorkplaceId = null; selectedWorkplaceName = null; selectedTbmSessionId = null; selectedVisitRequestId = null; + selectedProjectId = null; + projectSelected = false; updateLocationInfo(); - // 지도 데이터 로드 await Promise.all([ loadMapImage(), loadMapRegions(), @@ -158,6 +180,8 @@ async function onFactoryChange() { ]); renderMap(); + renderProjectList(); + updateStepStatus(); } /** @@ -175,7 +199,7 @@ async function loadMapImage() { if (data.success && data.data) { const selectedCategory = data.data.find(c => c.category_id == selectedFactoryId); if (selectedCategory && selectedCategory.layout_image) { - const baseUrl = (window.API_BASE_URL || 'http://localhost:30005').replace('/api', ''); + const baseUrl = (window.API_BASE_URL || 'http://localhost:30105').replace('/api', ''); const fullImageUrl = selectedCategory.layout_image.startsWith('http') ? selectedCategory.layout_image : `${baseUrl}${selectedCategory.layout_image}`; @@ -214,17 +238,13 @@ async function loadMapRegions() { * 오늘 TBM/출입신청 데이터 로드 */ async function loadTodayData() { - // 로컬 시간대 기준으로 오늘 날짜 구하기 (UTC가 아닌 한국 시간 기준) const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const today = `${year}-${month}-${day}`; - console.log('[신고페이지] 조회 날짜 (로컬):', today); - try { - // TBM 세션 로드 const tbmResponse = await fetch(`${API_BASE}/tbm/sessions/date/${today}`, { headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` } }); @@ -232,21 +252,13 @@ async function loadTodayData() { if (tbmResponse.ok) { const tbmData = await tbmResponse.json(); const sessions = tbmData.data || []; - - // TBM 세션 데이터를 가공하여 member_count 계산 todayWorkers = sessions.map(session => { const memberCount = session.team_member_count || 0; const leaderCount = session.leader_id ? 1 : 0; - return { - ...session, - member_count: memberCount + leaderCount - }; + return { ...session, member_count: memberCount + leaderCount }; }); - - console.log('[신고페이지] 로드된 TBM 작업:', todayWorkers.length, '건'); } - // 출입 신청 로드 const visitResponse = await fetch(`${API_BASE}/workplace-visits/requests`, { headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` } }); @@ -254,18 +266,14 @@ async function loadTodayData() { if (visitResponse.ok) { const visitData = await visitResponse.json(); todayVisitors = (visitData.data || []).filter(v => { - // 로컬 날짜로 비교 const visitDateObj = new Date(v.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 === today && (v.status === 'approved' || v.status === 'training_completed'); }); - - console.log('[신고페이지] 로드된 방문자:', todayVisitors.length, '건'); } } catch (error) { console.error('오늘 데이터 로드 실패:', error); @@ -273,7 +281,7 @@ async function loadTodayData() { } /** - * 둥근 모서리 사각형 그리기 (Canvas roundRect 폴리필) + * 둥근 모서리 사각형 그리기 */ function drawRoundRect(ctx, x, y, width, height, radius) { ctx.beginPath(); @@ -295,48 +303,34 @@ function drawRoundRect(ctx, x, y, width, height, radius) { function renderMap() { if (!canvas || !ctx) return; - // 컨테이너 너비 가져오기 const container = canvas.parentElement; - const containerWidth = container.clientWidth - 2; // border 고려 + const containerWidth = container.clientWidth - 2; const maxWidth = Math.min(containerWidth, 800); - // 이미지가 로드된 경우 이미지 비율에 맞춰 캔버스 크기 설정 if (canvasImage && canvasImage.complete && canvasImage.naturalWidth > 0) { const imgWidth = canvasImage.naturalWidth; const imgHeight = canvasImage.naturalHeight; - - // 스케일 계산 (maxWidth에 맞춤) const scale = imgWidth > maxWidth ? maxWidth / imgWidth : 1; canvas.width = imgWidth * scale; canvas.height = imgHeight * scale; - - // 이미지 그리기 ctx.drawImage(canvasImage, 0, 0, canvas.width, canvas.height); } else { - // 이미지가 없는 경우 기본 크기 canvas.width = maxWidth; - canvas.height = 400; - - // 배경 그리기 + canvas.height = 300; ctx.fillStyle = '#f3f4f6'; ctx.fillRect(0, 0, canvas.width, canvas.height); - - // 이미지 없음 안내 ctx.fillStyle = '#9ca3af'; ctx.font = '14px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('배치도 이미지가 없습니다', canvas.width / 2, canvas.height / 2); } - // 작업장 영역 그리기 (퍼센트 좌표 사용) 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 workerCount = workers.reduce((sum, w) => sum + (w.member_count || 0), 0); const visitorCount = visitors.reduce((sum, v) => sum + (v.visitor_count || 0), 0); - drawWorkplaceRegion(region, workerCount, visitorCount); }); } @@ -351,30 +345,27 @@ function drawWorkplaceRegion(region, workerCount, visitorCount) { const y2 = (region.y_end / 100) * canvas.height; const width = x2 - x1; const height = y2 - y1; - - // 선택된 작업장 하이라이트 const isSelected = region.workplace_id === selectedWorkplaceId; - // 색상 결정 (더 진하게 조정) let fillColor, strokeColor, textColor; if (isSelected) { - fillColor = 'rgba(34, 197, 94, 0.5)'; // 초록색 (선택됨) + fillColor = 'rgba(34, 197, 94, 0.5)'; strokeColor = 'rgb(22, 163, 74)'; textColor = '#15803d'; } else if (workerCount > 0 && visitorCount > 0) { - fillColor = 'rgba(34, 197, 94, 0.4)'; // 초록색 (작업+방문) + fillColor = 'rgba(34, 197, 94, 0.4)'; strokeColor = 'rgb(22, 163, 74)'; textColor = '#166534'; } else if (workerCount > 0) { - fillColor = 'rgba(59, 130, 246, 0.4)'; // 파란색 (작업만) + fillColor = 'rgba(59, 130, 246, 0.4)'; strokeColor = 'rgb(37, 99, 235)'; textColor = '#1e40af'; } else if (visitorCount > 0) { - fillColor = 'rgba(168, 85, 247, 0.4)'; // 보라색 (방문만) + fillColor = 'rgba(168, 85, 247, 0.4)'; strokeColor = 'rgb(147, 51, 234)'; textColor = '#7c3aed'; } else { - fillColor = 'rgba(107, 114, 128, 0.35)'; // 회색 (없음) - 더 진하게 + fillColor = 'rgba(107, 114, 128, 0.35)'; strokeColor = 'rgb(75, 85, 99)'; textColor = '#374151'; } @@ -382,17 +373,14 @@ function drawWorkplaceRegion(region, workerCount, visitorCount) { ctx.fillStyle = fillColor; ctx.strokeStyle = strokeColor; ctx.lineWidth = isSelected ? 4 : 2.5; - ctx.beginPath(); ctx.rect(x1, y1, width, height); ctx.fill(); ctx.stroke(); - // 작업장명 표시 (배경 추가로 가독성 향상) const centerX = x1 + width / 2; const centerY = y1 + height / 2; - // 텍스트 배경 ctx.font = 'bold 13px sans-serif'; const textMetrics = ctx.measureText(region.workplace_name); const textWidth = textMetrics.width + 12; @@ -402,26 +390,21 @@ function drawWorkplaceRegion(region, workerCount, visitorCount) { drawRoundRect(ctx, centerX - textWidth / 2, centerY - textHeight / 2, textWidth, textHeight, 4); ctx.fill(); - // 텍스트 ctx.fillStyle = textColor; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(region.workplace_name, centerX, centerY); - // 인원수 표시 const total = workerCount + visitorCount; if (total > 0) { - // 인원수 배경 ctx.font = 'bold 12px sans-serif'; const countText = `${total}명`; const countMetrics = ctx.measureText(countText); const countWidth = countMetrics.width + 10; const countHeight = 18; - ctx.fillStyle = strokeColor; drawRoundRect(ctx, centerX - countWidth / 2, centerY + 12, countWidth, countHeight, 4); ctx.fill(); - ctx.fillStyle = '#ffffff'; ctx.fillText(countText, centerX, centerY + 21); } @@ -432,10 +415,11 @@ function drawWorkplaceRegion(region, workerCount, visitorCount) { */ function onMapClick(e) { const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; - // 클릭된 영역 찾기 for (const region of mapRegions) { const x1 = (region.x_start / 100) * canvas.width; const y1 = (region.y_start / 100) * canvas.height; @@ -453,87 +437,177 @@ function onMapClick(e) { * 작업장 선택 */ function selectWorkplace(region) { - // 기타 위치 체크박스 해제 document.getElementById('useCustomLocation').checked = false; document.getElementById('customLocationInput').classList.remove('visible'); selectedWorkplaceId = region.workplace_id; selectedWorkplaceName = region.workplace_name; - - // 해당 작업장의 TBM/출입신청 확인 - const workers = todayWorkers.filter(w => w.workplace_id === region.workplace_id); - const visitors = todayVisitors.filter(v => v.workplace_id === region.workplace_id); - - if (workers.length > 0 || visitors.length > 0) { - // 작업 선택 모달 표시 - showWorkSelectionModal(workers, visitors); - } else { - selectedTbmSessionId = null; - selectedVisitRequestId = null; - } + selectedTbmSessionId = null; + selectedVisitRequestId = null; + selectedProjectId = null; + projectSelected = false; updateLocationInfo(); renderMap(); + renderProjectList(); updateStepStatus(); } /** - * 작업 선택 모달 표시 + * 프로젝트 목록 렌더링 (아코디언 방식) + * 범주 1: 오늘 TBM 등록 작업 (해당 위치) + * 범주 2: 활성 프로젝트 (DB) + * 범주 3: 프로젝트 모름 (하단) */ -function showWorkSelectionModal(workers, visitors) { - const modal = document.getElementById('workSelectionModal'); - const optionsList = document.getElementById('workOptionsList'); +function renderProjectList() { + const list = document.getElementById('projectList'); + list.innerHTML = ''; - optionsList.innerHTML = ''; + // 해당 위치의 TBM에서 프로젝트 정보 추출 + const workplaceWorkers = selectedWorkplaceId + ? todayWorkers.filter(w => w.workplace_id === selectedWorkplaceId) + : todayWorkers; + const registeredProjectIds = new Set( + workplaceWorkers.map(w => w.project_id).filter(Boolean) + ); - // TBM 작업 옵션 - workers.forEach(w => { - const option = document.createElement('div'); - option.className = 'work-option'; - const safeTaskName = escapeHtml(w.task_name || '작업'); - const safeProjectName = escapeHtml(w.project_name || ''); - const memberCount = parseInt(w.member_count) || 0; - option.innerHTML = ` -
TBM: ${safeTaskName}
-
${safeProjectName} - ${memberCount}명
- `; - option.onclick = () => { - selectedTbmSessionId = w.session_id; - selectedVisitRequestId = null; - closeWorkModal(); - updateLocationInfo(); - }; - optionsList.appendChild(option); - }); + // TBM 등록 프로젝트 + const tbmProjects = allProjects.filter(p => registeredProjectIds.has(p.project_id)); + // 나머지 활성 프로젝트 + const activeProjects = allProjects.filter(p => !registeredProjectIds.has(p.project_id)); - // 출입신청 옵션 - visitors.forEach(v => { - const option = document.createElement('div'); - option.className = 'work-option'; - const safeCompany = escapeHtml(v.visitor_company || '-'); - const safePurpose = escapeHtml(v.purpose_name || '방문'); - const visitorCount = parseInt(v.visitor_count) || 0; - option.innerHTML = ` -
출입: ${safeCompany}
-
${safePurpose} - ${visitorCount}명
- `; - option.onclick = () => { - selectedVisitRequestId = v.request_id; - selectedTbmSessionId = null; - closeWorkModal(); - updateLocationInfo(); - }; - optionsList.appendChild(option); - }); + // ─── 범주 1: TBM 등록 작업 ─── + if (tbmProjects.length > 0) { + const tbmGroup = createProjectGroup( + '🛠️', '오늘 TBM 등록 작업', tbmProjects.length, 'tbm-group', true + ); + const body = tbmGroup.querySelector('.project-group-body'); + tbmProjects.forEach(p => { + const tbm = workplaceWorkers.find(w => w.project_id === p.project_id); + const card = createProjectCard(p, tbm); + body.appendChild(card); + }); + list.appendChild(tbmGroup); + } - modal.classList.add('visible'); + // ─── 범주 2: 활성 프로젝트 ─── + if (activeProjects.length > 0) { + const activeGroup = createProjectGroup( + '📄', '활성 프로젝트', activeProjects.length, '', tbmProjects.length === 0 + ); + const body = activeGroup.querySelector('.project-group-body'); + activeProjects.forEach(p => { + const card = createProjectCard(p, null); + body.appendChild(card); + }); + list.appendChild(activeGroup); + } + + // 프로젝트가 아예 없을 때 + if (tbmProjects.length === 0 && activeProjects.length === 0) { + const emptyMsg = document.createElement('div'); + emptyMsg.className = 'project-empty'; + emptyMsg.textContent = '등록된 프로젝트가 없습니다'; + list.appendChild(emptyMsg); + } + + // ─── 범주 3: 프로젝트 모름 ─── + const unknownOption = document.createElement('div'); + unknownOption.className = 'project-skip'; + unknownOption.textContent = '프로젝트 여부 모름 (건너뛰기)'; + unknownOption.onclick = () => selectProject(null, null); + list.appendChild(unknownOption); } /** - * 작업 선택 모달 닫기 + * 아코디언 그룹 생성 */ -function closeWorkModal() { - document.getElementById('workSelectionModal').classList.remove('visible'); +function createProjectGroup(icon, title, count, extraClass, openByDefault) { + const group = document.createElement('div'); + group.className = 'project-group' + (extraClass ? ' ' + extraClass : ''); + if (openByDefault) group.classList.add('open'); + + group.innerHTML = ` +
+
+ ${icon} + ${title} + ${count}건 +
+ +
+
+ `; + + const header = group.querySelector('.project-group-header'); + header.onclick = () => group.classList.toggle('open'); + + return group; +} + +/** + * 프로젝트 카드 생성 + */ +function createProjectCard(project, tbmSession) { + const card = document.createElement('div'); + card.className = 'project-card'; + + const safeName = escapeHtml(project.project_name || ''); + const safeJobNo = escapeHtml(project.job_no || ''); + const safePm = escapeHtml(project.pm || ''); + + let html = `
${safeName}
`; + html += '
'; + if (safeJobNo) html += safeJobNo; + if (safePm) html += ` · PM: ${safePm}`; + html += '
'; + + if (tbmSession) { + const safeTask = escapeHtml(tbmSession.task_name || ''); + const count = parseInt(tbmSession.member_count) || 0; + html += `
TBM: ${safeTask} (${count}명)
`; + } + + card.innerHTML = html; + card._projectId = project.project_id; + card.onclick = (e) => { + e.stopPropagation(); + selectProject(project.project_id, tbmSession); + }; + + return card; +} + +/** + * 프로젝트 선택 처리 + */ +function selectProject(projectId, tbmSession) { + selectedProjectId = projectId; + projectSelected = true; + + if (tbmSession) { + selectedTbmSessionId = tbmSession.session_id; + } else { + selectedTbmSessionId = null; + } + selectedVisitRequestId = null; + + // 모든 선택 해제 → 현재 선택 표시 + const list = document.getElementById('projectList'); + list.querySelectorAll('.project-card, .project-skip').forEach(el => el.classList.remove('selected')); + + if (projectId === null) { + // 프로젝트 모름 + list.querySelector('.project-skip').classList.add('selected'); + } else { + // 카드에서 해당 프로젝트 찾아 선택 + list.querySelectorAll('.project-card').forEach(card => { + if (card._projectId === projectId) card.classList.add('selected'); + }); + } + + updateLocationInfo(); + updateStepStatus(); } /** @@ -551,15 +625,17 @@ function updateLocationInfo() { infoBox.classList.remove('empty'); let html = `선택된 위치: ${escapeHtml(selectedWorkplaceName)}`; + if (selectedProjectId) { + const proj = allProjects.find(p => p.project_id === selectedProjectId); + if (proj) { + html += `
프로젝트: ${escapeHtml(proj.project_name)}`; + } + } + if (selectedTbmSessionId) { const worker = todayWorkers.find(w => w.session_id === selectedTbmSessionId); if (worker) { - html += `
연결 작업: ${escapeHtml(worker.task_name || '-')} (TBM)`; - } - } else if (selectedVisitRequestId) { - const visitor = todayVisitors.find(v => v.request_id === selectedVisitRequestId); - if (visitor) { - html += `
연결 작업: ${escapeHtml(visitor.visitor_company || '-')} (출입)`; + html += `
TBM: ${escapeHtml(worker.task_name || '-')}`; } } @@ -578,13 +654,12 @@ function onTypeSelect(type) { selectedCategoryId = null; selectedCategoryName = null; selectedItemId = null; + customItemName = null; - // 버튼 상태 업데이트 document.querySelectorAll('.type-btn').forEach(btn => { btn.classList.toggle('selected', btn.dataset.type === type); }); - // 카테고리 로드 loadCategories(type); updateStepStatus(); } @@ -615,7 +690,6 @@ async function loadCategories(type) { function renderCategories(categories) { const container = document.getElementById('categoryContainer'); const grid = document.getElementById('categoryGrid'); - grid.innerHTML = ''; categories.forEach(cat => { @@ -637,13 +711,12 @@ function onCategorySelect(category) { selectedCategoryId = category.category_id; selectedCategoryName = category.category_name; selectedItemId = null; + customItemName = null; - // 버튼 상태 업데이트 document.querySelectorAll('.category-btn').forEach(btn => { btn.classList.toggle('selected', btn.textContent === category.category_name); }); - // 항목 로드 loadItems(category.category_id); updateStepStatus(); } @@ -675,7 +748,6 @@ function renderItems(items) { const grid = document.getElementById('itemGrid'); grid.innerHTML = ''; - // 기존 항목들 렌더링 items.forEach(item => { const btn = document.createElement('button'); btn.type = 'button'; @@ -686,7 +758,6 @@ function renderItems(items) { grid.appendChild(btn); }); - // 직접 입력 버튼 추가 const customBtn = document.createElement('button'); customBtn.type = 'button'; customBtn.className = 'item-btn custom-input-btn'; @@ -694,7 +765,6 @@ function renderItems(items) { customBtn.onclick = () => showCustomItemInput(); grid.appendChild(customBtn); - // 직접 입력 영역 숨기기 document.getElementById('customItemInput').style.display = 'none'; document.getElementById('customItemName').value = ''; customItemName = null; @@ -704,12 +774,10 @@ function renderItems(items) { * 항목 선택 */ function onItemSelect(item, btn) { - // 단일 선택 (기존 선택 해제) document.querySelectorAll('.item-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); - selectedItemId = item.item_id; - customItemName = null; // 기존 항목 선택 시 직접 입력 초기화 + customItemName = null; document.getElementById('customItemInput').style.display = 'none'; updateStepStatus(); } @@ -718,13 +786,9 @@ function onItemSelect(item, btn) { * 직접 입력 영역 표시 */ function showCustomItemInput() { - // 기존 선택 해제 document.querySelectorAll('.item-btn').forEach(b => b.classList.remove('selected')); document.querySelector('.custom-input-btn').classList.add('selected'); - selectedItemId = null; - - // 입력 영역 표시 document.getElementById('customItemInput').style.display = 'flex'; document.getElementById('customItemName').focus(); } @@ -735,21 +799,16 @@ function showCustomItemInput() { function confirmCustomItem() { const input = document.getElementById('customItemName'); const value = input.value.trim(); - if (!value) { alert('항목명을 입력해주세요.'); input.focus(); return; } - customItemName = value; - selectedItemId = null; // 커스텀 항목이므로 ID는 null - - // 입력 완료 표시 + selectedItemId = null; const customBtn = document.querySelector('.custom-input-btn'); - customBtn.textContent = `✓ ${value}`; + customBtn.textContent = `\u2713 ${value}`; customBtn.classList.add('selected'); - updateStepStatus(); } @@ -760,34 +819,63 @@ function cancelCustomItem() { document.getElementById('customItemInput').style.display = 'none'; document.getElementById('customItemName').value = ''; customItemName = null; - - // 직접 입력 버튼 원상복구 const customBtn = document.querySelector('.custom-input-btn'); customBtn.textContent = '+ 직접 입력'; customBtn.classList.remove('selected'); - updateStepStatus(); } /** - * 사진 선택 + * 사진 선택 (클라이언트에서 리사이징 후 저장) */ function onPhotoSelect(e) { const file = e.target.files[0]; if (!file) return; - const reader = new FileReader(); - reader.onload = (event) => { - photos[currentPhotoIndex] = event.target.result; + resizeImage(file, 1280, 0.8).then(dataUrl => { + photos[currentPhotoIndex] = dataUrl; updatePhotoSlot(currentPhotoIndex); - updateStepStatus(); // 제출 버튼 상태 업데이트 - }; - reader.readAsDataURL(file); - - // 입력 초기화 + updateStepStatus(); + }).catch(() => { + // 리사이징 실패 시 원본 사용 + const reader = new FileReader(); + reader.onload = (event) => { + photos[currentPhotoIndex] = event.target.result; + updatePhotoSlot(currentPhotoIndex); + updateStepStatus(); + }; + reader.readAsDataURL(file); + }); e.target.value = ''; } +/** + * 이미지 리사이징 (Canvas 이용) + * @param {File} file - 원본 파일 + * @param {number} maxSize - 최대 가로/세로 픽셀 + * @param {number} quality - JPEG 품질 (0~1) + * @returns {Promise} base64 data URL + */ +function resizeImage(file, maxSize, quality) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + let w = img.width, h = img.height; + if (w > maxSize || h > maxSize) { + if (w > h) { h = Math.round(h * maxSize / w); w = maxSize; } + else { w = Math.round(w * maxSize / h); h = maxSize; } + } + const cvs = document.createElement('canvas'); + cvs.width = w; + cvs.height = h; + cvs.getContext('2d').drawImage(img, 0, 0, w, h); + resolve(cvs.toDataURL('image/jpeg', quality)); + }; + img.onerror = reject; + img.src = URL.createObjectURL(file); + }); +} + /** * 사진 슬롯 업데이트 */ @@ -815,36 +903,35 @@ function updatePhotoSlot(index) { function removePhoto(index) { photos[index] = null; updatePhotoSlot(index); - updateStepStatus(); // 제출 버튼 상태 업데이트 + updateStepStatus(); } /** * 단계 상태 업데이트 + * 5단계: 유형(1) → 위치(2) → 프로젝트(3) → 항목(4) → 사진(5) */ function updateStepStatus() { const steps = document.querySelectorAll('.step'); const customLocation = document.getElementById('customLocation').value; const useCustom = document.getElementById('useCustomLocation').checked; - // Step 1: 위치 - const step1Complete = (useCustom && customLocation) || selectedWorkplaceId; - steps[0].classList.toggle('completed', step1Complete); - steps[1].classList.toggle('active', step1Complete); - - // Step 2: 유형 - const step2Complete = selectedType && selectedCategoryId; - steps[1].classList.toggle('completed', step2Complete); - steps[2].classList.toggle('active', step2Complete); - - // Step 3: 항목 (기존 항목 선택 또는 직접 입력) - const step3Complete = selectedItemId || customItemName; - steps[2].classList.toggle('completed', step3Complete); - steps[3].classList.toggle('active', step3Complete); - - // 제출 버튼 활성화 - const submitBtn = document.getElementById('submitBtn'); + const step1Complete = !!selectedType; + const step2Complete = (useCustom && customLocation) || !!selectedWorkplaceId; + const step3Complete = projectSelected; + const step4Complete = !!selectedCategoryId && (!!selectedItemId || !!customItemName); const hasPhoto = photos.some(p => p !== null); - submitBtn.disabled = !(step1Complete && step2Complete && step3Complete && hasPhoto); + + // Reset all + steps.forEach(s => { s.classList.remove('active', 'completed'); }); + + if (step1Complete) { steps[0].classList.add('completed'); } else { steps[0].classList.add('active'); } + if (step1Complete && step2Complete) { steps[1].classList.add('completed'); } else if (step1Complete) { steps[1].classList.add('active'); } + if (step1Complete && step2Complete && step3Complete) { steps[2].classList.add('completed'); } else if (step1Complete && step2Complete) { steps[2].classList.add('active'); } + if (step4Complete) { steps[3].classList.add('completed'); } else if (step1Complete && step2Complete && step3Complete) { steps[3].classList.add('active'); } + if (hasPhoto) { steps[4].classList.add('completed'); } else if (step4Complete) { steps[4].classList.add('active'); } + + const submitBtn = document.getElementById('submitBtn'); + submitBtn.disabled = !(step1Complete && step2Complete && step3Complete && step4Complete && hasPhoto); } /** @@ -864,11 +951,12 @@ async function submitReport() { factory_category_id: useCustom ? null : selectedFactoryId, workplace_id: useCustom ? null : selectedWorkplaceId, custom_location: useCustom ? customLocation : null, + project_id: selectedProjectId, tbm_session_id: selectedTbmSessionId, visit_request_id: selectedVisitRequestId, issue_category_id: selectedCategoryId, issue_item_id: selectedItemId, - custom_item_name: customItemName, // 직접 입력한 항목명 + custom_item_name: customItemName, additional_description: additionalDescription || null, photos: photos.filter(p => p !== null) }; @@ -886,15 +974,7 @@ async function submitReport() { if (data.success) { alert('신고가 등록되었습니다.'); - // 유형에 따라 다른 페이지로 리다이렉트 - if (selectedType === 'nonconformity') { - window.location.href = '/pages/work/nonconformity.html'; - } else if (selectedType === 'safety') { - window.location.href = '/pages/safety/report-status.html'; - } else { - // 기본: 뒤로가기 - history.back(); - } + window.location.href = '/pages/safety/report-status.html'; } else { throw new Error(data.error || '신고 등록 실패'); } @@ -918,8 +998,7 @@ document.addEventListener('DOMContentLoaded', () => { } }); -// 전역 함수 노출 (HTML onclick에서 호출용) -window.closeWorkModal = closeWorkModal; +// 전역 함수 노출 window.submitReport = submitReport; window.showCustomItemInput = showCustomItemInput; window.confirmCustomItem = confirmCustomItem; diff --git a/system2-report/web/nginx.conf b/system2-report/web/nginx.conf index bdd14a9..3851a6d 100644 --- a/system2-report/web/nginx.conf +++ b/system2-report/web/nginx.conf @@ -5,6 +5,9 @@ server { root /usr/share/nginx/html; index pages/safety/issue-report.html; + # 사진 업로드를 위한 body 크기 제한 (base64 인코딩 시 원본 대비 ~33% 증가) + client_max_body_size 50m; + # 정적 파일 캐시 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1h; @@ -17,7 +20,7 @@ server { add_header Cache-Control "no-store, no-cache, must-revalidate"; } - # System 1 API 프록시 (공장/작업장, TBM, 출입관리) + # System 1 API 프록시 (공장/작업장, TBM, 출입관리, 프로젝트) location /api/workplaces/ { proxy_pass http://system1-api:3005; proxy_set_header Host $host; @@ -26,6 +29,14 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } + location /api/projects/ { + proxy_pass http://system1-api:3005; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /api/tbm/ { proxy_pass http://system1-api:3005; proxy_set_header Host $host; @@ -52,7 +63,6 @@ server { } # System 2 API uploads (신고 사진 등) - # ^~ + 더 긴 prefix → /api/ 보다 우선 매칭 location ^~ /api/uploads/ { proxy_pass http://system2-api:3005/uploads/; proxy_set_header Host $host; @@ -62,7 +72,6 @@ server { } # System 1 uploads 프록시 (작업장 레이아웃 이미지 등) - # ^~ : 정적파일 캐시 regex보다 우선 매칭 location ^~ /uploads/ { proxy_pass http://system1-api:3005/uploads/; proxy_set_header Host $host; diff --git a/system2-report/web/pages/safety/issue-report.html b/system2-report/web/pages/safety/issue-report.html index 1a52e74..3ec8076 100644 --- a/system2-report/web/pages/safety/issue-report.html +++ b/system2-report/web/pages/safety/issue-report.html @@ -2,784 +2,616 @@ - - 신고 | (주)테크니컬코리아 - - - + + 신고 등록 | (주)테크니컬코리아 -
-
-
- + +
+ +

신고 등록

+ 신고현황 → +
- -
-
- 1 - 위치 선택 -
-
-
- 2 - 유형 선택 -
-
-
- 3 - 항목 선택 -
-
-
- 4 - 사진/설명 -
-
+ +
+
1유형
+
+
2위치
+
+
3작업
+
+
4항목
+
+
5사진
+
- -
-

- 1 - 발생 위치 선택 -

+ +
+
1신고 유형
+
+ + + +
+
-
- - -
+ +
+
2위치 선택
+ +
+ +
+
+ 지도에서 작업장을 클릭하여 위치를 선택하세요 +
+ +
+ +
+
-
- -
+ +
+
3프로젝트/작업 선택
+
+
위치를 먼저 선택하세요
+
+
-
- 지도에서 작업장을 클릭하여 위치를 선택하세요 -
- -
- - -
- -
- -
-
- - -
-

- 2 - 문제 유형 선택 -

- -
-
-
📋
-
부적합 사항
-
자재, 설계, 검사 관련
-
-
-
-
안전 관련
-
보호구, 위험구역, 안전수칙
-
-
-
🔧
-
시설설비 관련
-
설비 고장, 시설 파손
-
-
- - -
- - -
-

- 3 - 신고 항목 선택 -

-

해당하는 항목을 선택하거나 직접 입력하세요.

- -
-

먼저 카테고리를 선택하세요

-
- - - -
- - -
-

- 4 - 사진 및 추가 설명 -

- -
- -
-
- + - -
-
- + - -
-
- + - -
-
- + - -
-
- + - -
-
- -
- -
- - -
-
- - -
- - -
-
-
- - -
-
-

작업 선택

-

이 위치에 등록된 작업이 있습니다. 연결할 작업을 선택하세요.

-
- + +
+
4세부 항목
+
카테고리
+
+
+
항목
+
+
+ + +
- + +
+
5사진 및 상세
+
+
+
+
+
+
+
+
+
+
+
+
+
* 사진 1장 이상 필수 (최대 5장, 카메라 촬영 또는 앨범에서 선택)
+ +
+ + +
+ +
+ + + + + diff --git a/system2-report/web/pages/safety/report-status.html b/system2-report/web/pages/safety/report-status.html index c0f8c13..c9d87e9 100644 --- a/system2-report/web/pages/safety/report-status.html +++ b/system2-report/web/pages/safety/report-status.html @@ -286,9 +286,7 @@ - - + 안전 신고 - + + 신고하기
diff --git a/system3-nonconformance/web/issues-archive.html b/system3-nonconformance/web/issues-archive.html index e708223..730e669 100644 --- a/system3-nonconformance/web/issues-archive.html +++ b/system3-nonconformance/web/issues-archive.html @@ -3,7 +3,7 @@ - 폐기함 - 부적합 관리 + 폐기함 - 작업보고서 @@ -91,39 +91,39 @@
-
+
- +
-

완료

-

0

+

완료

+

0

-
+
- +
-

보관

-

0

+

보관

+

0

-
+
- +
-

취소

-

0

+

취소

+

0

-
+
- +
-

이번 달

-

0

+

이번 달

+

0

diff --git a/system3-nonconformance/web/issues-dashboard.html b/system3-nonconformance/web/issues-dashboard.html index 01ba01e..037b4b7 100644 --- a/system3-nonconformance/web/issues-dashboard.html +++ b/system3-nonconformance/web/issues-dashboard.html @@ -18,27 +18,29 @@ /* 대시보드 카드 스타일 */ .dashboard-card { - transition: all 0.2s ease; - background: #ffffff; - border-left: 4px solid #64748b; - } - - .dashboard-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } - /* 이슈 카드 스타일 */ - .issue-card { - transition: all 0.2s ease; - border-left: 4px solid transparent; - background: #ffffff; + .dashboard-card:hover { + transform: translateY(-5px); + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); } - + + /* 이슈 카드 스타일 (세련된 모던 스타일) */ + .issue-card { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border-left: 4px solid transparent; + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); + } + .issue-card:hover { - transform: translateY(-2px); - border-left-color: #475569; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transform: translateY(-8px) scale(1.02); + border-left-color: #3b82f6; + box-shadow: + 0 25px 50px -12px rgba(0, 0, 0, 0.15), + 0 0 0 1px rgba(59, 130, 246, 0.1), + 0 0 20px rgba(59, 130, 246, 0.1); } .issue-card label { @@ -90,7 +92,7 @@ } .progress-bar { - background: #475569; + background: linear-gradient(90deg, #10b981 0%, #059669 100%); transition: width 0.8s ease; } @@ -153,43 +155,55 @@
-
+
-

전체 진행 중

-

0

+

+ 전체 진행 중 +

+

+

0

- +
- -
+ +
-

오늘 신규

-

0

+

+ 오늘 신규 +

+

+

0

- +
- -
+ +
-

완료 대기

-

0

+

+ 완료 대기 +

+

+

0

- +
- -
+ +
-

지연 중

-

0

+

+ 지연 중 +

+

+

0

- +
diff --git a/system3-nonconformance/web/issues-inbox.html b/system3-nonconformance/web/issues-inbox.html index e2530a5..22be80e 100644 --- a/system3-nonconformance/web/issues-inbox.html +++ b/system3-nonconformance/web/issues-inbox.html @@ -3,7 +3,7 @@ - 수신함 - 부적합 관리 + 수신함 - 작업보고서 @@ -200,30 +200,30 @@
-
+
- +
-

금일 신규

-

0

+

금일 신규

+

0

-
+
- +
-

금일 처리

-

0

+

금일 처리

+

0

-
+
- +
-

미해결

-

0

+

미해결

+

0

diff --git a/system3-nonconformance/web/issues-management.html b/system3-nonconformance/web/issues-management.html index ceea6bb..0b59032 100644 --- a/system3-nonconformance/web/issues-management.html +++ b/system3-nonconformance/web/issues-management.html @@ -3,7 +3,7 @@ - 관리함 - 부적합 관리 + 관리함 - 작업보고서 @@ -273,39 +273,39 @@
-
+
- +
-

총 부적합

-

0

+

총 부적합

+

0

-
+
- +
-

진행 중

-

0

+

진행 중

+

0

-
+
- +
-

완료 대기

-

0

+

완료 대기

+

0

-
+
- +
-

완료됨

-

0

+

완료됨

+

0

diff --git a/system3-nonconformance/web/static/js/core/page-manager.js b/system3-nonconformance/web/static/js/core/page-manager.js index 0fc46e6..2c6d2c1 100644 --- a/system3-nonconformance/web/static/js/core/page-manager.js +++ b/system3-nonconformance/web/static/js/core/page-manager.js @@ -54,7 +54,7 @@ class PageManager { async checkAuthentication() { const token = localStorage.getItem('access_token'); if (!token) { - window.location.href = '/issues-dashboard.html'; + window.location.href = '/index.html'; return null; } @@ -69,7 +69,7 @@ class PageManager { console.error('인증 실패:', error); localStorage.removeItem('access_token'); localStorage.removeItem('currentUser'); - window.location.href = '/issues-dashboard.html'; + window.location.href = '/index.html'; return null; } } @@ -117,7 +117,7 @@ class PageManager { // 권한 시스템이 로드되지 않았으면 기본 페이지만 허용 if (!window.canAccessPage) { - return ['issues_dashboard', 'issues_inbox'].includes(pageId); + return ['issues_create', 'issues_view'].includes(pageId); } return window.canAccessPage(pageId); @@ -130,7 +130,11 @@ class PageManager { alert('이 페이지에 접근할 권한이 없습니다.'); // 기본적으로 접근 가능한 페이지로 이동 - window.location.href = '/issues-dashboard.html'; + if (window.canAccessPage && window.canAccessPage('issues_view')) { + window.location.href = '/issue-view.html'; + } else { + window.location.href = '/index.html'; + } } /** @@ -246,7 +250,7 @@ class PageManager { class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"> 다시 시도 - diff --git a/user-management/api/models/vacationModel.js b/user-management/api/models/vacationModel.js index cc4ee74..dcdc47e 100644 --- a/user-management/api/models/vacationModel.js +++ b/user-management/api/models/vacationModel.js @@ -82,12 +82,14 @@ async function getBalancesByYear(year) { const db = getPool(); const [rows] = await db.query( `SELECT vbd.*, vt.type_code, vt.type_name, vt.deduct_days, vt.priority, - w.worker_name, w.hire_date + w.worker_name, w.hire_date, w.department_id, + COALESCE(d.department_name, '미배정') AS department_name FROM vacation_balance_details vbd JOIN vacation_types vt ON vbd.vacation_type_id = vt.id JOIN workers w ON vbd.worker_id = w.worker_id + LEFT JOIN departments d ON w.department_id = d.department_id WHERE vbd.year = ? - ORDER BY w.worker_name ASC, vt.priority ASC`, + ORDER BY d.department_name ASC, w.worker_name ASC, vt.priority ASC`, [year] ); return rows; diff --git a/user-management/web/index.html b/user-management/web/index.html index 85dff27..ddc07b1 100644 --- a/user-management/web/index.html +++ b/user-management/web/index.html @@ -64,8 +64,8 @@ -
- - + + + - - - + + + + + +