- 페이지 폴더 재구성: safety/, attendance/ 폴더 신규 생성 - work/ → safety/: 이슈 신고, 출입 신청 관련 페이지 이동 - common/ → attendance/: 근태/휴가 관련 페이지 이동 - admin/ 정리: safety-* 파일들을 safety/로 이동 - 사이드바 네비게이션 메뉴 구현 - 카테고리별 메뉴: 작업관리, 안전관리, 근태관리, 시스템관리 - 접기/펼치기 기능 및 상태 저장 - 관리자 전용 메뉴 자동 표시/숨김 - 날씨 API 연동 (기상청 단기예보) - TBM 및 navbar에 현재 날씨 표시 - weatherService.js 추가 - 안전 체크리스트 확장 - 기본/날씨별/작업별 체크 유형 추가 - checklist-manage.html 페이지 추가 - 이슈 신고 시스템 구현 - workIssueController, workIssueModel, workIssueRoutes 추가 - DB 마이그레이션 파일 추가 (실행 대기) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
719 lines
20 KiB
JavaScript
719 lines
20 KiB
JavaScript
/**
|
|
* 안전 체크리스트 관리 페이지 스크립트
|
|
*
|
|
* 3가지 유형의 체크리스트 항목을 관리:
|
|
* 1. 기본 사항 - 항상 표시
|
|
* 2. 날씨별 - 날씨 조건에 따라 표시
|
|
* 3. 작업별 - 선택한 작업에 따라 표시
|
|
*
|
|
* @since 2026-02-02
|
|
*/
|
|
|
|
import { apiCall } from './api-config.js';
|
|
|
|
// 전역 상태
|
|
let allChecks = [];
|
|
let weatherConditions = [];
|
|
let workTypes = [];
|
|
let tasks = [];
|
|
let currentTab = 'basic';
|
|
let editingCheckId = null;
|
|
|
|
// 카테고리 정보
|
|
const CATEGORIES = {
|
|
PPE: { name: 'PPE (개인보호장비)', icon: '🦺' },
|
|
EQUIPMENT: { name: 'EQUIPMENT (장비점검)', icon: '🔧' },
|
|
ENVIRONMENT: { name: 'ENVIRONMENT (작업환경)', icon: '🏗️' },
|
|
EMERGENCY: { name: 'EMERGENCY (비상대응)', icon: '🚨' },
|
|
WEATHER: { name: 'WEATHER (날씨)', icon: '🌤️' },
|
|
TASK: { name: 'TASK (작업)', icon: '📋' }
|
|
};
|
|
|
|
// 날씨 아이콘 매핑
|
|
const WEATHER_ICONS = {
|
|
clear: '☀️',
|
|
rain: '🌧️',
|
|
snow: '❄️',
|
|
heat: '🔥',
|
|
cold: '🥶',
|
|
wind: '💨',
|
|
fog: '🌫️',
|
|
dust: '😷'
|
|
};
|
|
|
|
/**
|
|
* 페이지 초기화
|
|
*/
|
|
async function initPage() {
|
|
try {
|
|
console.log('📋 안전 체크리스트 관리 페이지 초기화...');
|
|
|
|
await Promise.all([
|
|
loadAllChecks(),
|
|
loadWeatherConditions(),
|
|
loadWorkTypes()
|
|
]);
|
|
|
|
renderCurrentTab();
|
|
console.log('✅ 초기화 완료. 체크항목:', allChecks.length, '개');
|
|
} catch (error) {
|
|
console.error('초기화 실패:', error);
|
|
showToast('데이터를 불러오는데 실패했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
// DOMContentLoaded 이벤트
|
|
document.addEventListener('DOMContentLoaded', initPage);
|
|
|
|
/**
|
|
* 모든 안전 체크 항목 로드
|
|
*/
|
|
async function loadAllChecks() {
|
|
try {
|
|
const response = await apiCall('/tbm/safety-checks');
|
|
if (response && response.success) {
|
|
allChecks = response.data || [];
|
|
console.log('✅ 체크 항목 로드:', allChecks.length, '개');
|
|
} else {
|
|
console.warn('체크 항목 응답 실패:', response);
|
|
allChecks = [];
|
|
}
|
|
} catch (error) {
|
|
console.error('체크 항목 로드 실패:', error);
|
|
allChecks = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 날씨 조건 목록 로드
|
|
*/
|
|
async function loadWeatherConditions() {
|
|
try {
|
|
const response = await apiCall('/tbm/weather/conditions');
|
|
if (response && response.success) {
|
|
weatherConditions = response.data || [];
|
|
populateWeatherSelects();
|
|
console.log('✅ 날씨 조건 로드:', weatherConditions.length, '개');
|
|
}
|
|
} catch (error) {
|
|
console.error('날씨 조건 로드 실패:', error);
|
|
weatherConditions = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 공정(작업 유형) 목록 로드
|
|
*/
|
|
async function loadWorkTypes() {
|
|
try {
|
|
const response = await apiCall('/daily-work-reports/work-types');
|
|
if (response && response.success) {
|
|
workTypes = response.data || [];
|
|
populateWorkTypeSelects();
|
|
console.log('✅ 공정 목록 로드:', workTypes.length, '개');
|
|
}
|
|
} catch (error) {
|
|
console.error('공정 목록 로드 실패:', error);
|
|
workTypes = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 날씨 조건 셀렉트 박스 채우기
|
|
*/
|
|
function populateWeatherSelects() {
|
|
const filterSelect = document.getElementById('weatherFilter');
|
|
const modalSelect = document.getElementById('weatherCondition');
|
|
|
|
const options = weatherConditions.map(wc =>
|
|
`<option value="${wc.condition_code}">${WEATHER_ICONS[wc.condition_code] || ''} ${wc.condition_name}</option>`
|
|
).join('');
|
|
|
|
if (filterSelect) {
|
|
filterSelect.innerHTML = `<option value="">모든 날씨 조건</option>${options}`;
|
|
}
|
|
|
|
if (modalSelect) {
|
|
modalSelect.innerHTML = options || '<option value="">날씨 조건 없음</option>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 공정 셀렉트 박스 채우기
|
|
*/
|
|
function populateWorkTypeSelects() {
|
|
const filterSelect = document.getElementById('workTypeFilter');
|
|
const modalSelect = document.getElementById('modalWorkType');
|
|
|
|
const options = workTypes.map(wt =>
|
|
`<option value="${wt.work_type_id}">${wt.work_type_name}</option>`
|
|
).join('');
|
|
|
|
if (filterSelect) {
|
|
filterSelect.innerHTML = `<option value="">공정 선택</option>${options}`;
|
|
}
|
|
|
|
if (modalSelect) {
|
|
modalSelect.innerHTML = `<option value="">공정 선택</option>${options}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 탭 전환
|
|
*/
|
|
function switchTab(tabName) {
|
|
currentTab = tabName;
|
|
|
|
// 탭 버튼 상태 업데이트
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
});
|
|
|
|
// 탭 콘텐츠 표시/숨김
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.toggle('active', content.id === `${tabName}Tab`);
|
|
});
|
|
|
|
renderCurrentTab();
|
|
}
|
|
|
|
/**
|
|
* 현재 탭 렌더링
|
|
*/
|
|
function renderCurrentTab() {
|
|
switch (currentTab) {
|
|
case 'basic':
|
|
renderBasicChecks();
|
|
break;
|
|
case 'weather':
|
|
renderWeatherChecks();
|
|
break;
|
|
case 'task':
|
|
renderTaskChecks();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기본 체크 항목 렌더링
|
|
*/
|
|
function renderBasicChecks() {
|
|
const container = document.getElementById('basicChecklistContainer');
|
|
const basicChecks = allChecks.filter(c => c.check_type === 'basic');
|
|
|
|
console.log('기본 체크항목:', basicChecks.length, '개');
|
|
|
|
if (basicChecks.length === 0) {
|
|
container.innerHTML = renderEmptyState('기본 체크 항목이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 카테고리별로 그룹화
|
|
const grouped = groupByCategory(basicChecks);
|
|
|
|
container.innerHTML = Object.entries(grouped).map(([category, items]) =>
|
|
renderChecklistGroup(category, items)
|
|
).join('');
|
|
}
|
|
|
|
/**
|
|
* 날씨별 체크 항목 렌더링
|
|
*/
|
|
function renderWeatherChecks() {
|
|
const container = document.getElementById('weatherChecklistContainer');
|
|
const filterValue = document.getElementById('weatherFilter')?.value;
|
|
|
|
let weatherChecks = allChecks.filter(c => c.check_type === 'weather');
|
|
|
|
if (filterValue) {
|
|
weatherChecks = weatherChecks.filter(c => c.weather_condition === filterValue);
|
|
}
|
|
|
|
if (weatherChecks.length === 0) {
|
|
container.innerHTML = renderEmptyState('날씨별 체크 항목이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 날씨 조건별로 그룹화
|
|
const grouped = groupByWeather(weatherChecks);
|
|
|
|
container.innerHTML = Object.entries(grouped).map(([condition, items]) => {
|
|
const conditionInfo = weatherConditions.find(wc => wc.condition_code === condition);
|
|
const icon = WEATHER_ICONS[condition] || '🌤️';
|
|
const name = conditionInfo?.condition_name || condition;
|
|
|
|
return renderChecklistGroup(`${icon} ${name}`, items, condition);
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* 작업별 체크 항목 렌더링
|
|
*/
|
|
function renderTaskChecks() {
|
|
const container = document.getElementById('taskChecklistContainer');
|
|
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
|
const taskId = document.getElementById('taskFilter')?.value;
|
|
|
|
let taskChecks = allChecks.filter(c => c.check_type === 'task');
|
|
|
|
if (taskId) {
|
|
taskChecks = taskChecks.filter(c => c.task_id == taskId);
|
|
} else if (workTypeId && tasks.length > 0) {
|
|
const workTypeTasks = tasks.filter(t => t.work_type_id == workTypeId);
|
|
const taskIds = workTypeTasks.map(t => t.task_id);
|
|
taskChecks = taskChecks.filter(c => taskIds.includes(c.task_id));
|
|
}
|
|
|
|
if (taskChecks.length === 0) {
|
|
container.innerHTML = renderEmptyState('작업별 체크 항목이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 작업별로 그룹화
|
|
const grouped = groupByTask(taskChecks);
|
|
|
|
container.innerHTML = Object.entries(grouped).map(([taskId, items]) => {
|
|
const task = tasks.find(t => t.task_id == taskId);
|
|
const taskName = task?.task_name || `작업 ${taskId}`;
|
|
|
|
return renderChecklistGroup(`📋 ${taskName}`, items, null, taskId);
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* 카테고리별 그룹화
|
|
*/
|
|
function groupByCategory(checks) {
|
|
return checks.reduce((acc, check) => {
|
|
const category = check.check_category || 'OTHER';
|
|
if (!acc[category]) acc[category] = [];
|
|
acc[category].push(check);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
/**
|
|
* 날씨 조건별 그룹화
|
|
*/
|
|
function groupByWeather(checks) {
|
|
return checks.reduce((acc, check) => {
|
|
const condition = check.weather_condition || 'other';
|
|
if (!acc[condition]) acc[condition] = [];
|
|
acc[condition].push(check);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
/**
|
|
* 작업별 그룹화
|
|
*/
|
|
function groupByTask(checks) {
|
|
return checks.reduce((acc, check) => {
|
|
const taskId = check.task_id || 0;
|
|
if (!acc[taskId]) acc[taskId] = [];
|
|
acc[taskId].push(check);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
/**
|
|
* 체크리스트 그룹 렌더링
|
|
*/
|
|
function renderChecklistGroup(title, items, weatherCondition = null, taskId = null) {
|
|
const categoryInfo = CATEGORIES[title] || { name: title, icon: '' };
|
|
const displayTitle = categoryInfo.name !== title ? categoryInfo.name : title;
|
|
const icon = categoryInfo.icon || '';
|
|
|
|
// 표시 순서로 정렬
|
|
items.sort((a, b) => (a.display_order || 0) - (b.display_order || 0));
|
|
|
|
return `
|
|
<div class="checklist-group">
|
|
<div class="group-header">
|
|
<div class="group-title">
|
|
<span class="group-icon">${icon}</span>
|
|
<span>${displayTitle}</span>
|
|
</div>
|
|
<span class="group-count">${items.length}개</span>
|
|
</div>
|
|
<div class="checklist-items">
|
|
${items.map(item => renderChecklistItem(item)).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 체크리스트 항목 렌더링
|
|
*/
|
|
function renderChecklistItem(item) {
|
|
const requiredBadge = item.is_required
|
|
? '<span class="item-badge badge-required">필수</span>'
|
|
: '<span class="item-badge badge-optional">선택</span>';
|
|
|
|
return `
|
|
<div class="checklist-item" data-check-id="${item.check_id}">
|
|
<div class="item-info">
|
|
<div class="item-name">${item.check_item}</div>
|
|
<div class="item-meta">
|
|
${requiredBadge}
|
|
${item.description ? `<span>${item.description}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
<button class="btn-icon btn-edit" onclick="openEditModal(${item.check_id})" title="수정">
|
|
✏️
|
|
</button>
|
|
<button class="btn-icon btn-delete" onclick="confirmDelete(${item.check_id})" title="삭제">
|
|
🗑️
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 빈 상태 렌더링
|
|
*/
|
|
function renderEmptyState(message) {
|
|
return `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">📋</div>
|
|
<p>${message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 날씨 필터 변경
|
|
*/
|
|
function filterByWeather() {
|
|
renderWeatherChecks();
|
|
}
|
|
|
|
/**
|
|
* 공정 필터 변경
|
|
*/
|
|
async function filterByWorkType() {
|
|
const workTypeId = document.getElementById('workTypeFilter')?.value;
|
|
const taskSelect = document.getElementById('taskFilter');
|
|
|
|
// workTypeId가 없거나 빈 문자열이면 early return
|
|
if (!workTypeId || workTypeId === '' || workTypeId === 'undefined') {
|
|
if (taskSelect) {
|
|
taskSelect.innerHTML = '<option value="">작업 선택</option>';
|
|
}
|
|
tasks = [];
|
|
renderTaskChecks();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiCall(`/tasks/work-type/${workTypeId}`);
|
|
if (response && response.success) {
|
|
tasks = response.data || [];
|
|
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
|
tasks.map(t => `<option value="${t.task_id}">${t.task_name}</option>`).join('');
|
|
}
|
|
} catch (error) {
|
|
console.error('작업 목록 로드 실패:', error);
|
|
tasks = [];
|
|
}
|
|
|
|
renderTaskChecks();
|
|
}
|
|
|
|
/**
|
|
* 작업 필터 변경
|
|
*/
|
|
function filterByTask() {
|
|
renderTaskChecks();
|
|
}
|
|
|
|
/**
|
|
* 모달의 작업 목록 로드
|
|
*/
|
|
async function loadModalTasks() {
|
|
const workTypeId = document.getElementById('modalWorkType')?.value;
|
|
const taskSelect = document.getElementById('modalTask');
|
|
|
|
// workTypeId가 없거나 빈 문자열이면 early return
|
|
if (!workTypeId || workTypeId === '' || workTypeId === 'undefined') {
|
|
if (taskSelect) {
|
|
taskSelect.innerHTML = '<option value="">작업 선택</option>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await apiCall(`/tasks/work-type/${workTypeId}`);
|
|
if (response && response.success) {
|
|
const modalTasks = response.data || [];
|
|
taskSelect.innerHTML = '<option value="">작업 선택</option>' +
|
|
modalTasks.map(t => `<option value="${t.task_id}">${t.task_name}</option>`).join('');
|
|
}
|
|
} catch (error) {
|
|
console.error('작업 목록 로드 실패:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 조건부 필드 토글
|
|
*/
|
|
function toggleConditionalFields() {
|
|
const checkType = document.querySelector('input[name="checkType"]:checked')?.value;
|
|
|
|
document.getElementById('basicFields').classList.toggle('show', checkType === 'basic');
|
|
document.getElementById('weatherFields').classList.toggle('show', checkType === 'weather');
|
|
document.getElementById('taskFields').classList.toggle('show', checkType === 'task');
|
|
}
|
|
|
|
/**
|
|
* 추가 모달 열기
|
|
*/
|
|
function openAddModal() {
|
|
editingCheckId = null;
|
|
document.getElementById('modalTitle').textContent = '체크 항목 추가';
|
|
|
|
// 폼 초기화
|
|
document.getElementById('checkForm').reset();
|
|
document.getElementById('checkId').value = '';
|
|
|
|
// 현재 탭에 맞는 유형 선택
|
|
const typeRadio = document.querySelector(`input[name="checkType"][value="${currentTab}"]`);
|
|
if (typeRadio) {
|
|
typeRadio.checked = true;
|
|
}
|
|
|
|
toggleConditionalFields();
|
|
showModal();
|
|
}
|
|
|
|
/**
|
|
* 수정 모달 열기
|
|
*/
|
|
async function openEditModal(checkId) {
|
|
editingCheckId = checkId;
|
|
const check = allChecks.find(c => c.check_id === checkId);
|
|
|
|
if (!check) {
|
|
showToast('항목을 찾을 수 없습니다.', 'error');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('modalTitle').textContent = '체크 항목 수정';
|
|
document.getElementById('checkId').value = checkId;
|
|
|
|
// 유형 선택
|
|
const typeRadio = document.querySelector(`input[name="checkType"][value="${check.check_type}"]`);
|
|
if (typeRadio) {
|
|
typeRadio.checked = true;
|
|
}
|
|
|
|
toggleConditionalFields();
|
|
|
|
// 카테고리
|
|
if (check.check_type === 'basic') {
|
|
document.getElementById('checkCategory').value = check.check_category || 'PPE';
|
|
}
|
|
|
|
// 날씨 조건
|
|
if (check.check_type === 'weather') {
|
|
document.getElementById('weatherCondition').value = check.weather_condition || '';
|
|
}
|
|
|
|
// 작업
|
|
if (check.check_type === 'task' && check.task_id) {
|
|
// 먼저 공정 찾기 (task를 통해)
|
|
const task = tasks.find(t => t.task_id === check.task_id);
|
|
if (task) {
|
|
document.getElementById('modalWorkType').value = task.work_type_id;
|
|
await loadModalTasks();
|
|
document.getElementById('modalTask').value = check.task_id;
|
|
}
|
|
}
|
|
|
|
// 공통 필드
|
|
document.getElementById('checkItem').value = check.check_item || '';
|
|
document.getElementById('checkDescription').value = check.description || '';
|
|
document.getElementById('isRequired').checked = check.is_required === 1 || check.is_required === true;
|
|
document.getElementById('displayOrder').value = check.display_order || 0;
|
|
|
|
showModal();
|
|
}
|
|
|
|
/**
|
|
* 모달 표시
|
|
*/
|
|
function showModal() {
|
|
document.getElementById('checkModal').style.display = 'flex';
|
|
}
|
|
|
|
/**
|
|
* 모달 닫기
|
|
*/
|
|
function closeModal() {
|
|
document.getElementById('checkModal').style.display = 'none';
|
|
editingCheckId = null;
|
|
}
|
|
|
|
/**
|
|
* 체크 항목 저장
|
|
*/
|
|
async function saveCheck() {
|
|
const checkType = document.querySelector('input[name="checkType"]:checked')?.value;
|
|
const checkItem = document.getElementById('checkItem').value.trim();
|
|
|
|
if (!checkItem) {
|
|
showToast('체크 항목을 입력해주세요.', 'error');
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
check_type: checkType,
|
|
check_item: checkItem,
|
|
description: document.getElementById('checkDescription').value.trim() || null,
|
|
is_required: document.getElementById('isRequired').checked,
|
|
display_order: parseInt(document.getElementById('displayOrder').value) || 0
|
|
};
|
|
|
|
// 유형별 추가 데이터
|
|
switch (checkType) {
|
|
case 'basic':
|
|
data.check_category = document.getElementById('checkCategory').value;
|
|
break;
|
|
case 'weather':
|
|
data.check_category = 'WEATHER';
|
|
data.weather_condition = document.getElementById('weatherCondition').value;
|
|
if (!data.weather_condition) {
|
|
showToast('날씨 조건을 선택해주세요.', 'error');
|
|
return;
|
|
}
|
|
break;
|
|
case 'task':
|
|
data.check_category = 'TASK';
|
|
data.task_id = document.getElementById('modalTask').value;
|
|
if (!data.task_id) {
|
|
showToast('작업을 선택해주세요.', 'error');
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
try {
|
|
let response;
|
|
if (editingCheckId) {
|
|
// 수정
|
|
response = await apiCall(`/tbm/safety-checks/${editingCheckId}`, 'PUT', data);
|
|
} else {
|
|
// 추가
|
|
response = await apiCall('/tbm/safety-checks', 'POST', data);
|
|
}
|
|
|
|
if (response && response.success) {
|
|
showToast(editingCheckId ? '항목이 수정되었습니다.' : '항목이 추가되었습니다.', 'success');
|
|
closeModal();
|
|
await loadAllChecks();
|
|
renderCurrentTab();
|
|
} else {
|
|
showToast(response?.message || '저장에 실패했습니다.', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('저장 실패:', error);
|
|
showToast('저장 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 삭제 확인
|
|
*/
|
|
function confirmDelete(checkId) {
|
|
const check = allChecks.find(c => c.check_id === checkId);
|
|
|
|
if (!check) {
|
|
showToast('항목을 찾을 수 없습니다.', 'error');
|
|
return;
|
|
}
|
|
|
|
if (confirm(`"${check.check_item}" 항목을 삭제하시겠습니까?`)) {
|
|
deleteCheck(checkId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 체크 항목 삭제
|
|
*/
|
|
async function deleteCheck(checkId) {
|
|
try {
|
|
const response = await apiCall(`/tbm/safety-checks/${checkId}`, 'DELETE');
|
|
|
|
if (response && response.success) {
|
|
showToast('항목이 삭제되었습니다.', 'success');
|
|
await loadAllChecks();
|
|
renderCurrentTab();
|
|
} else {
|
|
showToast(response?.message || '삭제에 실패했습니다.', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('삭제 실패:', error);
|
|
showToast('삭제 중 오류가 발생했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 토스트 메시지 표시
|
|
*/
|
|
function showToast(message, type = 'info') {
|
|
// 기존 토스트 제거
|
|
const existingToast = document.querySelector('.toast-message');
|
|
if (existingToast) {
|
|
existingToast.remove();
|
|
}
|
|
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast-message toast-${type}`;
|
|
toast.textContent = message;
|
|
toast.style.cssText = `
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
padding: 12px 24px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
z-index: 9999;
|
|
animation: fadeInUp 0.3s ease;
|
|
${type === 'success' ? 'background: #10b981; color: white;' : ''}
|
|
${type === 'error' ? 'background: #ef4444; color: white;' : ''}
|
|
${type === 'info' ? 'background: #3b82f6; color: white;' : ''}
|
|
`;
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'fadeOut 0.3s ease';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// 모달 외부 클릭 시 닫기
|
|
document.getElementById('checkModal')?.addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
// HTML onclick에서 호출할 수 있도록 전역에 노출
|
|
window.switchTab = switchTab;
|
|
window.openAddModal = openAddModal;
|
|
window.openEditModal = openEditModal;
|
|
window.closeModal = closeModal;
|
|
window.saveCheck = saveCheck;
|
|
window.confirmDelete = confirmDelete;
|
|
window.filterByWeather = filterByWeather;
|
|
window.filterByWorkType = filterByWorkType;
|
|
window.filterByTask = filterByTask;
|
|
window.loadModalTasks = loadModalTasks;
|
|
window.toggleConditionalFields = toggleConditionalFields;
|