- 부적합 API 호출 형식 수정 (카테고리/아이템 추가 시) - 부적합 저장 시 내부 플래그 제거 후 백엔드 전송 - 기본 부적합 객체 구조 수정 (category_id, item_id 추가) - 날씨 API 시간대 수정 (UTC → KST 변환) - 신고 카테고리 관리 페이지 추가 (/pages/admin/issue-categories.html) - 부적합 입력 UI 개선 (대분류→소분류 캐스케이딩 선택) - 저장된 부적합 분리 표시 및 수정/삭제 기능 - 디버깅 로그 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
921 lines
27 KiB
JavaScript
921 lines
27 KiB
JavaScript
/**
|
|
* 신고 등록 페이지 JavaScript
|
|
* URL 파라미터 ?type=nonconformity 또는 ?type=safety로 유형 사전 선택 지원
|
|
*/
|
|
|
|
// API 설정
|
|
const API_BASE = window.API_BASE_URL || 'http://localhost:20005/api';
|
|
|
|
// 상태 변수
|
|
let selectedFactoryId = null;
|
|
let selectedWorkplaceId = null;
|
|
let selectedWorkplaceName = null;
|
|
let selectedType = null; // 'nonconformity' | 'safety'
|
|
let selectedCategoryId = null;
|
|
let selectedCategoryName = null;
|
|
let selectedItemId = null;
|
|
let selectedTbmSessionId = null;
|
|
let selectedVisitRequestId = null;
|
|
let photos = [null, null, null, null, null];
|
|
let customItemName = null; // 직접 입력한 항목명
|
|
|
|
// 지도 관련 변수
|
|
let canvas, ctx, canvasImage;
|
|
let mapRegions = [];
|
|
let todayWorkers = [];
|
|
let todayVisitors = [];
|
|
|
|
// 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();
|
|
|
|
// 공장 목록 로드
|
|
await loadFactories();
|
|
|
|
// URL 파라미터에서 유형 확인 및 자동 선택
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const preselectedType = urlParams.get('type');
|
|
if (preselectedType === 'nonconformity' || preselectedType === 'safety') {
|
|
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();
|
|
}
|
|
});
|
|
|
|
// 유형 버튼 클릭
|
|
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 loadFactories() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/workplaces/categories/active/list`, {
|
|
headers: { 'Authorization': `Bearer ${localStorage.getItem('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;
|
|
updateLocationInfo();
|
|
|
|
// 지도 데이터 로드
|
|
await Promise.all([
|
|
loadMapImage(),
|
|
loadMapRegions(),
|
|
loadTodayData()
|
|
]);
|
|
|
|
renderMap();
|
|
}
|
|
|
|
/**
|
|
* 배치도 이미지 로드
|
|
*/
|
|
async function loadMapImage() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/workplaces/categories`, {
|
|
headers: { 'Authorization': `Bearer ${localStorage.getItem('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:20005').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 ${localStorage.getItem('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() {
|
|
// 로컬 시간대 기준으로 오늘 날짜 구하기 (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 ${localStorage.getItem('token')}` }
|
|
});
|
|
|
|
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
|
|
};
|
|
});
|
|
|
|
console.log('[신고페이지] 로드된 TBM 작업:', todayWorkers.length, '건');
|
|
}
|
|
|
|
// 출입 신청 로드
|
|
const visitResponse = await fetch(`${API_BASE}/workplace-visits/requests`, {
|
|
headers: { 'Authorization': `Bearer ${localStorage.getItem('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');
|
|
});
|
|
|
|
console.log('[신고페이지] 로드된 방문자:', todayVisitors.length, '건');
|
|
}
|
|
} catch (error) {
|
|
console.error('오늘 데이터 로드 실패:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 둥근 모서리 사각형 그리기 (Canvas roundRect 폴리필)
|
|
*/
|
|
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; // border 고려
|
|
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;
|
|
|
|
// 배경 그리기
|
|
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);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 작업장 영역 그리기
|
|
*/
|
|
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 x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
// 클릭된 영역 찾기
|
|
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;
|
|
|
|
// 해당 작업장의 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;
|
|
}
|
|
|
|
updateLocationInfo();
|
|
renderMap();
|
|
updateStepStatus();
|
|
}
|
|
|
|
/**
|
|
* 작업 선택 모달 표시
|
|
*/
|
|
function showWorkSelectionModal(workers, visitors) {
|
|
const modal = document.getElementById('workSelectionModal');
|
|
const optionsList = document.getElementById('workOptionsList');
|
|
|
|
optionsList.innerHTML = '';
|
|
|
|
// TBM 작업 옵션
|
|
workers.forEach(w => {
|
|
const option = document.createElement('div');
|
|
option.className = 'work-option';
|
|
option.innerHTML = `
|
|
<div class="work-option-title">TBM: ${w.task_name || '작업'}</div>
|
|
<div class="work-option-desc">${w.project_name || ''} - ${w.member_count || 0}명</div>
|
|
`;
|
|
option.onclick = () => {
|
|
selectedTbmSessionId = w.session_id;
|
|
selectedVisitRequestId = null;
|
|
closeWorkModal();
|
|
updateLocationInfo();
|
|
};
|
|
optionsList.appendChild(option);
|
|
});
|
|
|
|
// 출입신청 옵션
|
|
visitors.forEach(v => {
|
|
const option = document.createElement('div');
|
|
option.className = 'work-option';
|
|
option.innerHTML = `
|
|
<div class="work-option-title">출입: ${v.visitor_company}</div>
|
|
<div class="work-option-desc">${v.purpose_name || '방문'} - ${v.visitor_count || 0}명</div>
|
|
`;
|
|
option.onclick = () => {
|
|
selectedVisitRequestId = v.request_id;
|
|
selectedTbmSessionId = null;
|
|
closeWorkModal();
|
|
updateLocationInfo();
|
|
};
|
|
optionsList.appendChild(option);
|
|
});
|
|
|
|
modal.classList.add('visible');
|
|
}
|
|
|
|
/**
|
|
* 작업 선택 모달 닫기
|
|
*/
|
|
function closeWorkModal() {
|
|
document.getElementById('workSelectionModal').classList.remove('visible');
|
|
}
|
|
|
|
/**
|
|
* 선택된 위치 정보 업데이트
|
|
*/
|
|
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 = `<strong>선택된 위치:</strong> ${customLocation}`;
|
|
} else if (selectedWorkplaceName) {
|
|
infoBox.classList.remove('empty');
|
|
let html = `<strong>선택된 위치:</strong> ${selectedWorkplaceName}`;
|
|
|
|
if (selectedTbmSessionId) {
|
|
const worker = todayWorkers.find(w => w.session_id === selectedTbmSessionId);
|
|
if (worker) {
|
|
html += `<br><span style="color: var(--primary-600);">연결 작업: ${worker.task_name} (TBM)</span>`;
|
|
}
|
|
} else if (selectedVisitRequestId) {
|
|
const visitor = todayVisitors.find(v => v.request_id === selectedVisitRequestId);
|
|
if (visitor) {
|
|
html += `<br><span style="color: var(--primary-600);">연결 작업: ${visitor.visitor_company} (출입)</span>`;
|
|
}
|
|
}
|
|
|
|
infoBox.innerHTML = html;
|
|
} else {
|
|
infoBox.classList.add('empty');
|
|
infoBox.textContent = '지도에서 작업장을 클릭하여 위치를 선택하세요';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 유형 선택
|
|
*/
|
|
function onTypeSelect(type) {
|
|
selectedType = type;
|
|
selectedCategoryId = null;
|
|
selectedCategoryName = null;
|
|
selectedItemId = 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 ${localStorage.getItem('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;
|
|
|
|
// 버튼 상태 업데이트
|
|
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 ${localStorage.getItem('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; // 커스텀 항목이므로 ID는 null
|
|
|
|
// 입력 완료 표시
|
|
const customBtn = document.querySelector('.custom-input-btn');
|
|
customBtn.textContent = `✓ ${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;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
photos[currentPhotoIndex] = event.target.result;
|
|
updatePhotoSlot(currentPhotoIndex);
|
|
updateStepStatus(); // 제출 버튼 상태 업데이트
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
// 입력 초기화
|
|
e.target.value = '';
|
|
}
|
|
|
|
/**
|
|
* 사진 슬롯 업데이트
|
|
*/
|
|
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(); // 제출 버튼 상태 업데이트
|
|
}
|
|
|
|
/**
|
|
* 단계 상태 업데이트
|
|
*/
|
|
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 hasPhoto = photos.some(p => p !== null);
|
|
submitBtn.disabled = !(step1Complete && step2Complete && step3Complete && 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,
|
|
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 ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
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();
|
|
}
|
|
} 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();
|
|
});
|
|
}
|
|
});
|
|
|
|
// 전역 함수 노출 (HTML onclick에서 호출용)
|
|
window.closeWorkModal = closeWorkModal;
|
|
window.submitReport = submitReport;
|
|
window.showCustomItemInput = showCustomItemInput;
|
|
window.confirmCustomItem = confirmCustomItem;
|
|
window.cancelCustomItem = cancelCustomItem;
|