/** * 신고 등록 페이지 JavaScript * 흐름: 유형(1) → 위치(2) → 프로젝트(3) → 카테고리/항목(4) → 사진/상세(5) → 제출 * URL 파라미터 ?type=nonconformity|safety|facility 로 유형 사전 선택 지원 */ // 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' | '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 canvas, ctx, canvasImage; let mapRegions = []; let todayWorkers = []; let todayVisitors = []; // 프로젝트 관련 변수 let allProjects = []; // 전체 활성 프로젝트 목록 // DOM 요소 let factorySelect, issueMapCanvas; let photoInput, currentPhotoIndex; // 초기화 document.addEventListener('DOMContentLoaded', async () => { factorySelect = document.getElementById('factorySelect'); issueMapCanvas = document.getElementById('issueMapCanvas'); photoInput = document.getElementById('photoInput'); 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 (['nonconformity', 'safety', 'facility'].includes(preselectedType)) { onTypeSelect(preselectedType); } }); /** * 이벤트 리스너 설정 */ 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(); // 기타 위치에서도 프로젝트 목록 표시 } }); // 유형 버튼 클릭 document.querySelectorAll('.type-btn').forEach(btn => { btn.addEventListener('click', () => onTypeSelect(btn.dataset.type)); }); // 사진 슬롯 클릭 document.querySelectorAll('.photo-slot').forEach(slot => { slot.addEventListener('click', (e) => { if (e.target.classList.contains('remove-btn')) return; currentPhotoIndex = parseInt(slot.dataset.index); photoInput.click(); }); }); // 사진 삭제 버튼 document.querySelectorAll('.photo-slot .remove-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const slot = btn.closest('.photo-slot'); const index = parseInt(slot.dataset.index); removePhoto(index); }); }); // 사진 선택 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 = []; } } /** * 공장 목록 로드 */ async function loadFactories() { try { const response = await fetch(`${API_BASE}/workplaces/categories/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) { data.data.forEach(factory => { const option = document.createElement('option'); option.value = factory.category_id; option.textContent = factory.category_name; factorySelect.appendChild(option); }); if (data.data.length > 0) { factorySelect.value = data.data[0].category_id; onFactoryChange(); } } } catch (error) { console.error('공장 목록 로드 실패:', error); } } /** * 공장 변경 시 */ 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(), loadTodayData() ]); renderMap(); renderProjectList(); updateStepStatus(); } /** * 배치도 이미지 로드 */ async function loadMapImage() { try { const response = await fetch(`${API_BASE}/workplaces/categories`, { headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` } }); if (!response.ok) return; const data = await response.json(); 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:30105').replace('/api', ''); const fullImageUrl = selectedCategory.layout_image.startsWith('http') ? selectedCategory.layout_image : `${baseUrl}${selectedCategory.layout_image}`; canvasImage = new Image(); canvasImage.onload = () => renderMap(); canvasImage.src = fullImageUrl; } } } catch (error) { console.error('배치도 이미지 로드 실패:', error); } } /** * 지도 영역 로드 */ async function loadMapRegions() { try { const response = await fetch(`${API_BASE}/workplaces/categories/${selectedFactoryId}/map-regions`, { headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` } }); if (!response.ok) return; const data = await response.json(); if (data.success) { mapRegions = data.data || []; } } catch (error) { console.error('지도 영역 로드 실패:', error); } } /** * 오늘 TBM/출입신청 데이터 로드 */ async function loadTodayData() { 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}`; try { const tbmResponse = await fetch(`${API_BASE}/tbm/sessions/date/${today}`, { headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` } }); if (tbmResponse.ok) { const tbmData = await tbmResponse.json(); const sessions = tbmData.data || []; todayWorkers = sessions.map(session => { const memberCount = session.team_member_count || 0; return { ...session, member_count: memberCount }; }); } const visitResponse = await fetch(`${API_BASE}/workplace-visits/requests`, { headers: { 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` } }); 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'); }); } } catch (error) { console.error('오늘 데이터 로드 실패:', error); } } /** * 둥근 모서리 사각형 그리기 */ function drawRoundRect(ctx, x, y, width, height, radius) { ctx.beginPath(); ctx.moveTo(x + radius, y); ctx.lineTo(x + width - radius, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius); ctx.lineTo(x + width, y + height - radius); ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); ctx.lineTo(x + radius, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius); ctx.lineTo(x, y + radius); ctx.quadraticCurveTo(x, y, x + radius, y); ctx.closePath(); } /** * 지도 렌더링 */ function renderMap() { if (!canvas || !ctx) return; const container = canvas.parentElement; 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; 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 = 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); }); // 모바일일 때 전체화면 지도 버튼 표시 const triggerBtn = document.getElementById('landscapeTriggerBtn'); if (triggerBtn && window.innerWidth <= 768 && canvasImage && canvasImage.complete && mapRegions.length > 0) { triggerBtn.style.display = 'inline-flex'; } } /** * 작업장 영역 그리기 */ function drawWorkplaceRegion(region, workerCount, visitorCount) { const x1 = (region.x_start / 100) * canvas.width; const y1 = (region.y_start / 100) * canvas.height; const x2 = (region.x_end / 100) * canvas.width; 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)'; strokeColor = 'rgb(22, 163, 74)'; textColor = '#15803d'; } else if (workerCount > 0 && visitorCount > 0) { 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)'; strokeColor = 'rgb(37, 99, 235)'; textColor = '#1e40af'; } else if (visitorCount > 0) { fillColor = 'rgba(168, 85, 247, 0.4)'; strokeColor = 'rgb(147, 51, 234)'; textColor = '#7c3aed'; } else { fillColor = 'rgba(107, 114, 128, 0.35)'; strokeColor = 'rgb(75, 85, 99)'; textColor = '#374151'; } 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; const textHeight = 20; ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; 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); } } /** * 지도 클릭 처리 */ function onMapClick(e) { const rect = canvas.getBoundingClientRect(); 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; const x2 = (region.x_end / 100) * canvas.width; const y2 = (region.y_end / 100) * canvas.height; if (x >= x1 && x <= x2 && y >= y1 && y <= y2) { selectWorkplace(region); return; } } } /** * 작업장 선택 */ function selectWorkplace(region) { document.getElementById('useCustomLocation').checked = false; document.getElementById('customLocationInput').classList.remove('visible'); selectedWorkplaceId = region.workplace_id; selectedWorkplaceName = region.workplace_name; selectedTbmSessionId = null; selectedVisitRequestId = null; selectedProjectId = null; projectSelected = false; updateLocationInfo(); renderMap(); renderProjectList(); updateStepStatus(); } // ==================== 가로모드 전체화면 지도 ==================== function openLandscapeMap() { if (!canvasImage || !canvasImage.complete || mapRegions.length === 0) return; const overlay = document.getElementById('landscapeOverlay'); const inner = document.getElementById('landscapeInner'); const lCanvas = document.getElementById('landscapeCanvas'); if (!overlay || !lCanvas) return; overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; // 물리적 가로모드 여부 const isPhysicalLandscape = window.innerWidth > window.innerHeight; inner.className = 'landscape-inner ' + (isPhysicalLandscape ? 'no-rotate' : 'rotated'); // 가용 영역 const headerH = 52; const pad = 16; let availW, availH; if (isPhysicalLandscape) { availW = window.innerWidth - pad * 2; availH = window.innerHeight - headerH - pad * 2; } else { availW = window.innerHeight - pad * 2; availH = window.innerWidth - headerH - pad * 2; } // 이미지 비율 유지 const imgRatio = canvasImage.naturalWidth / canvasImage.naturalHeight; let cw, ch; if (availW / availH > imgRatio) { ch = availH; cw = ch * imgRatio; } else { cw = availW; ch = cw / imgRatio; } lCanvas.width = Math.round(cw); lCanvas.height = Math.round(ch); drawLandscapeMap(); lCanvas.ontouchstart = handleLandscapeTouchStart; lCanvas.onclick = handleLandscapeClick; } function drawLandscapeMap() { const lCanvas = document.getElementById('landscapeCanvas'); if (!lCanvas || !canvasImage) return; const lCtx = lCanvas.getContext('2d'); lCtx.drawImage(canvasImage, 0, 0, lCanvas.width, lCanvas.height); 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); const x1 = (region.x_start / 100) * lCanvas.width; const y1 = (region.y_start / 100) * lCanvas.height; const x2 = (region.x_end / 100) * lCanvas.width; const y2 = (region.y_end / 100) * lCanvas.height; const w = x2 - x1; const h = y2 - y1; const isSelected = region.workplace_id === selectedWorkplaceId; let fillColor, strokeColor, textColor; if (isSelected) { 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)'; strokeColor = 'rgb(22, 163, 74)'; textColor = '#166534'; } else if (workerCount > 0) { 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)'; strokeColor = 'rgb(147, 51, 234)'; textColor = '#7c3aed'; } else { fillColor = 'rgba(107, 114, 128, 0.35)'; strokeColor = 'rgb(75, 85, 99)'; textColor = '#374151'; } lCtx.fillStyle = fillColor; lCtx.strokeStyle = strokeColor; lCtx.lineWidth = isSelected ? 4 : 2.5; lCtx.beginPath(); lCtx.rect(x1, y1, w, h); lCtx.fill(); lCtx.stroke(); const centerX = x1 + w / 2; const centerY = y1 + h / 2; lCtx.font = 'bold 13px sans-serif'; const textMetrics = lCtx.measureText(region.workplace_name); const textWidth = textMetrics.width + 12; const textHeight = 20; lCtx.fillStyle = 'rgba(255, 255, 255, 0.9)'; drawRoundRect(lCtx, centerX - textWidth / 2, centerY - textHeight / 2, textWidth, textHeight, 4); lCtx.fill(); lCtx.fillStyle = textColor; lCtx.textAlign = 'center'; lCtx.textBaseline = 'middle'; lCtx.fillText(region.workplace_name, centerX, centerY); const total = workerCount + visitorCount; if (total > 0) { lCtx.font = 'bold 12px sans-serif'; const countText = `${total}명`; const countMetrics = lCtx.measureText(countText); const countWidth = countMetrics.width + 10; const countHeight = 18; lCtx.fillStyle = strokeColor; drawRoundRect(lCtx, centerX - countWidth / 2, centerY + 12, countWidth, countHeight, 4); lCtx.fill(); lCtx.fillStyle = '#ffffff'; lCtx.fillText(countText, centerX, centerY + 21); } }); } function getLandscapeCoords(clientX, clientY) { const lCanvas = document.getElementById('landscapeCanvas'); if (!lCanvas) return null; const rect = lCanvas.getBoundingClientRect(); const inner = document.getElementById('landscapeInner'); const isRotated = inner.classList.contains('rotated'); if (!isRotated) { const scaleX = lCanvas.width / rect.width; const scaleY = lCanvas.height / rect.height; return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY }; } const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const dx = clientX - centerX; const dy = clientY - centerY; const inverseDx = dy; const inverseDy = -dx; const unrotatedW = rect.height; const unrotatedH = rect.width; const canvasX = (inverseDx + unrotatedW / 2) / unrotatedW * lCanvas.width; const canvasY = (inverseDy + unrotatedH / 2) / unrotatedH * lCanvas.height; return { x: canvasX, y: canvasY }; } function handleLandscapeTouchStart(e) { e.preventDefault(); const touch = e.touches[0]; const coords = getLandscapeCoords(touch.clientX, touch.clientY); if (coords) doLandscapeHitTest(coords.x, coords.y); } function handleLandscapeClick(e) { const coords = getLandscapeCoords(e.clientX, e.clientY); if (coords) doLandscapeHitTest(coords.x, coords.y); } function doLandscapeHitTest(cx, cy) { const lCanvas = document.getElementById('landscapeCanvas'); if (!lCanvas) return; for (const region of mapRegions) { const x1 = (region.x_start / 100) * lCanvas.width; const y1 = (region.y_start / 100) * lCanvas.height; const x2 = (region.x_end / 100) * lCanvas.width; const y2 = (region.y_end / 100) * lCanvas.height; if (cx >= x1 && cx <= x2 && cy >= y1 && cy <= y2) { selectWorkplace(region); drawLandscapeMap(); setTimeout(() => closeLandscapeMap(), 300); return; } } } function closeLandscapeMap() { const overlay = document.getElementById('landscapeOverlay'); if (overlay) overlay.style.display = 'none'; const lCanvas = document.getElementById('landscapeCanvas'); if (lCanvas) { lCanvas.ontouchstart = null; lCanvas.onclick = null; } document.body.style.overflow = ''; } /** * 프로젝트 목록 렌더링 (아코디언 방식) * 범주 1: 오늘 TBM 등록 작업 (해당 위치) * 범주 2: 활성 프로젝트 (DB) * 범주 3: 프로젝트 모름 (하단) */ function renderProjectList() { const list = document.getElementById('projectList'); list.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 등록 프로젝트 const tbmProjects = allProjects.filter(p => registeredProjectIds.has(p.project_id)); // 나머지 활성 프로젝트 const activeProjects = allProjects.filter(p => !registeredProjectIds.has(p.project_id)); // ─── 범주 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); } // ─── 범주 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 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(); } /** * 선택된 위치 정보 업데이트 */ function updateLocationInfo() { const infoBox = document.getElementById('selectedLocationInfo'); const customLocation = document.getElementById('customLocation').value; const useCustom = document.getElementById('useCustomLocation').checked; if (useCustom && customLocation) { infoBox.classList.remove('empty'); infoBox.innerHTML = `선택된 위치: ${escapeHtml(customLocation)}`; } else if (selectedWorkplaceName) { 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 += `
TBM: ${escapeHtml(worker.task_name || '-')}`; } } infoBox.innerHTML = html; } else { infoBox.classList.add('empty'); infoBox.textContent = '지도에서 작업장을 클릭하여 위치를 선택하세요'; } } /** * 유형 선택 */ function onTypeSelect(type) { selectedType = 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(); } /** * 카테고리 로드 */ async function loadCategories(type) { try { const response = await fetch(`${API_BASE}/work-issues/categories/type/${type}`, { 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) { renderCategories(data.data); } } catch (error) { console.error('카테고리 로드 실패:', error); } } /** * 카테고리 렌더링 */ function renderCategories(categories) { const container = document.getElementById('categoryContainer'); const grid = document.getElementById('categoryGrid'); grid.innerHTML = ''; categories.forEach(cat => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'category-btn'; btn.textContent = cat.category_name; btn.onclick = () => onCategorySelect(cat); grid.appendChild(btn); }); container.style.display = 'block'; } /** * 카테고리 선택 */ 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(); } /** * 항목 로드 */ async function loadItems(categoryId) { try { const response = await fetch(`${API_BASE}/work-issues/items/category/${categoryId}`, { 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) { renderItems(data.data); } } catch (error) { console.error('항목 로드 실패:', error); } } /** * 항목 렌더링 */ function renderItems(items) { const grid = document.getElementById('itemGrid'); grid.innerHTML = ''; items.forEach(item => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'item-btn'; btn.textContent = item.item_name; btn.dataset.severity = item.severity; btn.onclick = () => onItemSelect(item, btn); grid.appendChild(btn); }); const customBtn = document.createElement('button'); customBtn.type = 'button'; customBtn.className = 'item-btn custom-input-btn'; customBtn.textContent = '+ 직접 입력'; customBtn.onclick = () => showCustomItemInput(); grid.appendChild(customBtn); document.getElementById('customItemInput').style.display = 'none'; document.getElementById('customItemName').value = ''; customItemName = null; } /** * 항목 선택 */ function onItemSelect(item, btn) { document.querySelectorAll('.item-btn').forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); selectedItemId = item.item_id; customItemName = null; document.getElementById('customItemInput').style.display = 'none'; updateStepStatus(); } /** * 직접 입력 영역 표시 */ 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(); } /** * 직접 입력 확인 */ function confirmCustomItem() { const input = document.getElementById('customItemName'); const value = input.value.trim(); if (!value) { alert('항목명을 입력해주세요.'); input.focus(); return; } customItemName = value; selectedItemId = null; const customBtn = document.querySelector('.custom-input-btn'); customBtn.textContent = `\u2713 ${value}`; customBtn.classList.add('selected'); updateStepStatus(); } /** * 직접 입력 취소 */ 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; resizeImage(file, 1280, 0.8).then(dataUrl => { photos[currentPhotoIndex] = dataUrl; updatePhotoSlot(currentPhotoIndex); 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); }); } /** * 사진 슬롯 업데이트 */ function updatePhotoSlot(index) { const slot = document.querySelector(`.photo-slot[data-index="${index}"]`); if (photos[index]) { slot.classList.add('has-photo'); let img = slot.querySelector('img'); if (!img) { img = document.createElement('img'); slot.insertBefore(img, slot.firstChild); } img.src = photos[index]; } else { slot.classList.remove('has-photo'); const img = slot.querySelector('img'); if (img) img.remove(); } } /** * 사진 삭제 */ function removePhoto(index) { photos[index] = null; updatePhotoSlot(index); 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; const step1Complete = !!selectedType; const step2Complete = (useCustom && customLocation) || !!selectedWorkplaceId; const step3Complete = projectSelected; const step4Complete = !!selectedCategoryId && (!!selectedItemId || !!customItemName); const hasPhoto = photos.some(p => p !== null); // 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); } /** * 신고 제출 */ async function submitReport() { const submitBtn = document.getElementById('submitBtn'); submitBtn.disabled = true; submitBtn.textContent = '제출 중...'; try { const useCustom = document.getElementById('useCustomLocation').checked; const customLocation = document.getElementById('customLocation').value; const additionalDescription = document.getElementById('additionalDescription').value; const requestBody = { 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, additional_description: additionalDescription || null, photos: photos.filter(p => p !== null) }; const response = await fetch(`${API_BASE}/work-issues`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${(window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))}` }, body: JSON.stringify(requestBody) }); const data = await response.json(); if (data.success) { document.getElementById('successModal').style.display = 'flex'; } else { throw new Error(data.error || '신고 등록 실패'); } } catch (error) { console.error('신고 제출 실패:', error); alert('신고 등록에 실패했습니다: ' + error.message); } finally { submitBtn.disabled = false; submitBtn.textContent = '신고 제출'; } } // 기타 위치 입력 시 위치 정보 업데이트 document.addEventListener('DOMContentLoaded', () => { const customLocationInput = document.getElementById('customLocation'); if (customLocationInput) { customLocationInput.addEventListener('input', () => { updateLocationInfo(); updateStepStatus(); }); } }); // 전역 함수 노출 window.submitReport = submitReport; window.showCustomItemInput = showCustomItemInput; window.confirmCustomItem = confirmCustomItem; window.cancelCustomItem = cancelCustomItem;