- 신고 제출 후 alert → 성공 모달로 교체 (신고현황/새신고 버튼) - cross-nav.js: tkreport 페이지 상단 크로스시스템 네비게이션 배너 - report-status.html: AI 신고 도우미 버튼 추가 - common-header.js: tkqc 헤더에 "신고" 외부 링크 추가 - 배포 스크립트/가이드 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1217 lines
38 KiB
JavaScript
1217 lines
38 KiB
JavaScript
/**
|
|
* 신고 등록 페이지 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 = `
|
|
<div class="project-group-header">
|
|
<div class="group-left">
|
|
<span class="group-icon">${icon}</span>
|
|
<span class="group-title">${title}</span>
|
|
<span class="group-count">${count}건</span>
|
|
</div>
|
|
<span class="group-arrow">▼</span>
|
|
</div>
|
|
<div class="project-group-body"></div>
|
|
`;
|
|
|
|
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 = `<div class="project-card-title">${safeName}</div>`;
|
|
html += '<div class="project-card-desc">';
|
|
if (safeJobNo) html += safeJobNo;
|
|
if (safePm) html += ` · PM: ${safePm}`;
|
|
html += '</div>';
|
|
|
|
if (tbmSession) {
|
|
const safeTask = escapeHtml(tbmSession.task_name || '');
|
|
const count = parseInt(tbmSession.member_count) || 0;
|
|
html += `<div class="tbm-info">TBM: ${safeTask} (${count}명)</div>`;
|
|
}
|
|
|
|
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 = `<strong>선택된 위치:</strong> ${escapeHtml(customLocation)}`;
|
|
} else if (selectedWorkplaceName) {
|
|
infoBox.classList.remove('empty');
|
|
let html = `<strong>선택된 위치:</strong> ${escapeHtml(selectedWorkplaceName)}`;
|
|
|
|
if (selectedProjectId) {
|
|
const proj = allProjects.find(p => p.project_id === selectedProjectId);
|
|
if (proj) {
|
|
html += `<br><span style="color:#059669;">프로젝트: ${escapeHtml(proj.project_name)}</span>`;
|
|
}
|
|
}
|
|
|
|
if (selectedTbmSessionId) {
|
|
const worker = todayWorkers.find(w => w.session_id === selectedTbmSessionId);
|
|
if (worker) {
|
|
html += `<br><span style="color:#2563eb;">TBM: ${escapeHtml(worker.task_name || '-')}</span>`;
|
|
}
|
|
}
|
|
|
|
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<string>} 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;
|