✨ 주요 변경사항: - 단일 docker-compose.yml로 통합 (로컬/시놀로지 환경 지원) - 시놀로지 볼륨 매핑 설정 (volume1: 이미지, volume3: 데이터) - 통합 배포 가이드 및 자동 배포 스크립트 추가 - 완전한 Memos 스타일 워크플로우 구현 🎯 새로운 기능: - 📝 메모 작성 (upload.html) - 이미지 업로드 지원 - 📥 수신함 (inbox.html) - 메모 편집 및 Todo/보드 변환 - ✅ Todo 목록 (todo-list.html) - 오늘 할 일 관리 - 📋 보드 (board.html) - 프로젝트 관리, 접기/펼치기, 이미지 지원 - 📚 아카이브 (archive.html) - 완료된 보드 보관 - 🔐 초기 설정 화면 - 관리자 계정 생성 🔧 기술적 개선: - 이미지 업로드/편집 완전 지원 - 반응형 디자인 및 모바일 최적화 - 보드 완료 후 자동 숨김 처리 - 메모 편집 시 제목 필드 제거 - 테스트 로그인 버튼 제거 (프로덕션 준비) - 과거 코드 정리 (TodoService, CalendarSyncService 등) 📦 배포 관련: - env.synology.example - 시놀로지 환경 설정 템플릿 - SYNOLOGY_DEPLOYMENT_GUIDE.md - 상세한 배포 가이드 - deploy-synology.sh - 원클릭 자동 배포 스크립트 - Nginx 정적 파일 서빙 및 이미지 프록시 설정 🗑️ 정리된 파일: - 사용하지 않는 HTML 페이지들 (dashboard, calendar, checklist 등) - 복잡한 통합 서비스들 (integrations 폴더) - 중복된 시놀로지 설정 파일들
789 lines
28 KiB
JavaScript
789 lines
28 KiB
JavaScript
/**
|
|
* Todo 관리 기능
|
|
*/
|
|
|
|
let todos = [];
|
|
let currentPhoto = null;
|
|
let currentFilter = 'all';
|
|
|
|
// 페이지 로드 시 초기화
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
setupTodoForm();
|
|
setupPhotoUpload();
|
|
setupFilters();
|
|
updateItemCounts();
|
|
loadRegisteredItems();
|
|
});
|
|
|
|
// Todo 폼 설정
|
|
function setupTodoForm() {
|
|
const todoForm = document.getElementById('todoForm');
|
|
if (todoForm) {
|
|
todoForm.addEventListener('submit', handleTodoSubmit);
|
|
}
|
|
}
|
|
|
|
// 사진 업로드 설정
|
|
function setupPhotoUpload() {
|
|
const cameraInput = document.getElementById('cameraInput');
|
|
const galleryInput = document.getElementById('galleryInput');
|
|
|
|
if (cameraInput) {
|
|
cameraInput.addEventListener('change', handlePhotoUpload);
|
|
}
|
|
|
|
if (galleryInput) {
|
|
galleryInput.addEventListener('change', handlePhotoUpload);
|
|
}
|
|
}
|
|
|
|
// 필터 설정
|
|
function setupFilters() {
|
|
// 필터 탭 클릭 이벤트는 HTML에서 onclick으로 처리
|
|
}
|
|
|
|
// Todo 제출 처리
|
|
async function handleTodoSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
const content = document.getElementById('todoContent').value.trim();
|
|
if (!content) {
|
|
alert('할일 내용을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showLoading(true);
|
|
|
|
const todoData = {
|
|
content: content,
|
|
photo: currentPhoto,
|
|
status: 'draft',
|
|
created_at: new Date().toISOString()
|
|
};
|
|
|
|
// 임시 저장 (백엔드 구현 전까지)
|
|
const newTodo = {
|
|
id: Date.now(),
|
|
...todoData,
|
|
user_id: window.currentUser?.id || 1
|
|
};
|
|
|
|
todos.unshift(newTodo);
|
|
|
|
// 실제 API 호출 (백엔드 구현 후 사용)
|
|
/*
|
|
const newTodo = await TodoAPI.createTodo(todoData);
|
|
todos.unshift(newTodo);
|
|
*/
|
|
|
|
// 폼 초기화 및 목록 업데이트
|
|
clearForm();
|
|
loadRegisteredItems();
|
|
updateItemCounts();
|
|
|
|
// 성공 메시지
|
|
showToast('항목이 등록되었습니다!', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('할일 추가 실패:', error);
|
|
alert(error.message || '할일 추가에 실패했습니다.');
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
// 사진 업로드 처리
|
|
async function handlePhotoUpload(event) {
|
|
const files = event.target.files;
|
|
if (!files || files.length === 0) return;
|
|
|
|
const file = files[0];
|
|
|
|
try {
|
|
showLoading(true);
|
|
|
|
// 이미지 압축
|
|
const compressedImage = await ImageUtils.compressImage(file, {
|
|
maxWidth: 800,
|
|
maxHeight: 600,
|
|
quality: 0.8
|
|
});
|
|
|
|
currentPhoto = compressedImage;
|
|
|
|
// 미리보기 표시
|
|
const previewContainer = document.getElementById('photoPreview');
|
|
const previewImage = document.getElementById('previewImage');
|
|
|
|
if (previewContainer && previewImage) {
|
|
previewImage.src = compressedImage;
|
|
previewContainer.classList.remove('hidden');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('이미지 처리 실패:', error);
|
|
alert('이미지 처리에 실패했습니다.');
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
// 카메라 열기
|
|
function openCamera() {
|
|
const cameraInput = document.getElementById('cameraInput');
|
|
if (cameraInput) {
|
|
cameraInput.click();
|
|
}
|
|
}
|
|
|
|
// 갤러리 열기
|
|
function openGallery() {
|
|
const galleryInput = document.getElementById('galleryInput');
|
|
if (galleryInput) {
|
|
galleryInput.click();
|
|
}
|
|
}
|
|
|
|
// 사진 제거
|
|
function removePhoto() {
|
|
currentPhoto = null;
|
|
|
|
const previewContainer = document.getElementById('photoPreview');
|
|
const previewImage = document.getElementById('previewImage');
|
|
|
|
if (previewContainer) {
|
|
previewContainer.classList.add('hidden');
|
|
}
|
|
|
|
if (previewImage) {
|
|
previewImage.src = '';
|
|
}
|
|
|
|
// 파일 입력 초기화
|
|
const cameraInput = document.getElementById('cameraInput');
|
|
const galleryInput = document.getElementById('galleryInput');
|
|
|
|
if (cameraInput) cameraInput.value = '';
|
|
if (galleryInput) galleryInput.value = '';
|
|
}
|
|
|
|
// 폼 초기화
|
|
function clearForm() {
|
|
const todoForm = document.getElementById('todoForm');
|
|
if (todoForm) {
|
|
todoForm.reset();
|
|
}
|
|
|
|
removePhoto();
|
|
}
|
|
|
|
// Todo 목록 로드
|
|
async function loadTodos() {
|
|
try {
|
|
// 실제 API 호출
|
|
todos = await TodoAPI.getTodos(currentFilter);
|
|
|
|
renderTodos();
|
|
|
|
} catch (error) {
|
|
console.error('할일 목록 로드 실패:', error);
|
|
showToast('할일 목록을 불러오는데 실패했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
// Todo 목록 렌더링
|
|
function renderTodos() {
|
|
const todoList = document.getElementById('todoList');
|
|
const emptyState = document.getElementById('emptyState');
|
|
|
|
if (!todoList || !emptyState) return;
|
|
|
|
// 필터링
|
|
const filteredTodos = todos.filter(todo => {
|
|
if (currentFilter === 'all') return true;
|
|
if (currentFilter === 'active') return ['draft', 'scheduled', 'active', 'delayed'].includes(todo.status);
|
|
if (currentFilter === 'completed') return todo.status === 'completed';
|
|
return todo.status === currentFilter;
|
|
});
|
|
|
|
// 빈 상태 처리
|
|
if (filteredTodos.length === 0) {
|
|
todoList.innerHTML = '';
|
|
emptyState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
emptyState.classList.add('hidden');
|
|
|
|
// Todo 항목 렌더링
|
|
todoList.innerHTML = filteredTodos.map(todo => `
|
|
<div class="todo-item p-4 hover:bg-gray-50 transition-colors">
|
|
<div class="flex items-start space-x-4">
|
|
<!-- 체크박스 -->
|
|
<button onclick="toggleTodo('${todo.id}')" class="mt-1 flex-shrink-0">
|
|
<i class="fas ${todo.status === 'completed' ? 'fa-check-circle text-green-500' : 'fa-circle text-gray-300'} text-lg"></i>
|
|
</button>
|
|
|
|
<!-- 사진 (있는 경우) -->
|
|
${todo.photo ? `
|
|
<div class="flex-shrink-0">
|
|
<img src="${todo.photo}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- 내용 -->
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-gray-900 ${todo.status === 'completed' ? 'line-through text-gray-500' : ''}">${todo.content}</p>
|
|
<div class="flex items-center space-x-3 mt-2 text-sm text-gray-500">
|
|
<span class="status-${todo.status}">
|
|
<i class="fas ${getStatusIcon(todo.status)} mr-1"></i>${getStatusText(todo.status)}
|
|
</span>
|
|
<span>${formatDate(todo.created_at)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 액션 버튼 -->
|
|
<div class="flex-shrink-0 flex space-x-2">
|
|
${todo.status !== 'completed' ? `
|
|
<button onclick="editTodo('${todo.id}')" class="text-gray-400 hover:text-blue-500">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
` : ''}
|
|
<button onclick="deleteTodo('${todo.id}')" class="text-gray-400 hover:text-red-500">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Todo 상태 토글
|
|
async function toggleTodo(id) {
|
|
try {
|
|
const todo = todos.find(t => t.id === id);
|
|
if (!todo) return;
|
|
|
|
const newStatus = todo.status === 'completed' ? 'active' : 'completed';
|
|
|
|
// 임시 업데이트
|
|
todo.status = newStatus;
|
|
|
|
// 실제 API 호출 (백엔드 구현 후 사용)
|
|
/*
|
|
await TodoAPI.updateTodo(id, { status: newStatus });
|
|
*/
|
|
|
|
renderTodos();
|
|
showToast(newStatus === 'completed' ? '할일을 완료했습니다!' : '할일을 다시 활성화했습니다!', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('할일 상태 변경 실패:', error);
|
|
showToast('상태 변경에 실패했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
// Todo 삭제
|
|
async function deleteTodo(id) {
|
|
if (!confirm('정말로 이 할일을 삭제하시겠습니까?')) return;
|
|
|
|
try {
|
|
// 임시 삭제
|
|
todos = todos.filter(t => t.id !== id);
|
|
|
|
// 실제 API 호출 (백엔드 구현 후 사용)
|
|
/*
|
|
await TodoAPI.deleteTodo(id);
|
|
*/
|
|
|
|
renderTodos();
|
|
showToast('할일이 삭제되었습니다.', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('할일 삭제 실패:', error);
|
|
showToast('삭제에 실패했습니다.', 'error');
|
|
}
|
|
}
|
|
|
|
// Todo 편집 (향후 구현)
|
|
function editTodo(id) {
|
|
// TODO: 편집 모달 또는 인라인 편집 구현
|
|
console.log('편집 기능 구현 예정:', id);
|
|
}
|
|
|
|
// 필터 변경
|
|
function filterTodos(filter) {
|
|
currentFilter = filter;
|
|
|
|
// 탭 활성화 상태 변경
|
|
document.querySelectorAll('.filter-tab').forEach(tab => {
|
|
tab.classList.remove('active', 'bg-white', 'text-blue-600');
|
|
tab.classList.add('text-gray-600');
|
|
});
|
|
|
|
event.target.classList.add('active', 'bg-white', 'text-blue-600');
|
|
event.target.classList.remove('text-gray-600');
|
|
|
|
renderTodos();
|
|
}
|
|
|
|
// 상태 아이콘 반환
|
|
function getStatusIcon(status) {
|
|
const icons = {
|
|
draft: 'fa-edit',
|
|
scheduled: 'fa-calendar',
|
|
active: 'fa-play',
|
|
completed: 'fa-check',
|
|
delayed: 'fa-clock'
|
|
};
|
|
return icons[status] || 'fa-circle';
|
|
}
|
|
|
|
// 상태 텍스트 반환
|
|
function getStatusText(status) {
|
|
const texts = {
|
|
draft: '검토 필요',
|
|
scheduled: '예정됨',
|
|
active: '진행중',
|
|
completed: '완료됨',
|
|
delayed: '지연됨'
|
|
};
|
|
return texts[status] || '알 수 없음';
|
|
}
|
|
|
|
// 날짜 포맷팅
|
|
function formatDate(dateString) {
|
|
if (!dateString) return '날짜 없음';
|
|
const date = new Date(dateString);
|
|
if (isNaN(date.getTime())) return '날짜 없음';
|
|
|
|
const now = new Date();
|
|
const diffTime = now - date;
|
|
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffDays === 0) return '오늘';
|
|
if (diffDays === 1) return '어제';
|
|
if (diffDays < 7) return `${diffDays}일 전`;
|
|
|
|
return date.toLocaleDateString('ko-KR');
|
|
}
|
|
|
|
// 토스트 메시지 표시
|
|
function showToast(message, type = 'info') {
|
|
// 간단한 alert으로 대체 (향후 토스트 UI 구현)
|
|
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
|
|
if (type === 'error') {
|
|
alert(message);
|
|
}
|
|
}
|
|
|
|
// 페이지 이동 함수
|
|
function goToPage(pageType) {
|
|
const pages = {
|
|
'todo': 'todo.html',
|
|
'calendar': 'calendar.html',
|
|
'checklist': 'checklist.html'
|
|
};
|
|
|
|
if (pages[pageType]) {
|
|
window.location.href = pages[pageType];
|
|
} else {
|
|
console.error('Unknown page type:', pageType);
|
|
}
|
|
}
|
|
|
|
// 대시보드로 이동
|
|
function goToDashboard() {
|
|
window.location.href = 'upload.html';
|
|
}
|
|
|
|
// 분류 센터로 이동
|
|
function goToClassify() {
|
|
window.location.href = 'classify.html';
|
|
}
|
|
|
|
// 항목 등록 후 인덱스 업데이트
|
|
async function updateItemCounts() {
|
|
try {
|
|
// 무한 로딩 방지: 토큰이 없으면 API 요청하지 않음
|
|
const token = localStorage.getItem('authToken');
|
|
if (!token) {
|
|
console.log('토큰이 없어서 카운트 업데이트를 건너뜁니다.');
|
|
// 토큰이 없으면 0개로 표시
|
|
const todoCountEl = document.getElementById('todoCount');
|
|
const calendarCountEl = document.getElementById('calendarCount');
|
|
const checklistCountEl = document.getElementById('checklistCount');
|
|
|
|
if (todoCountEl) todoCountEl.textContent = '0개';
|
|
if (calendarCountEl) calendarCountEl.textContent = '0개';
|
|
if (checklistCountEl) checklistCountEl.textContent = '0개';
|
|
return;
|
|
}
|
|
|
|
// API에서 실제 데이터 가져와서 카운트
|
|
const items = await TodoAPI.getTodos();
|
|
|
|
const todoCount = items.filter(item => item.category === 'todo').length;
|
|
const calendarCount = items.filter(item => item.category === 'calendar').length;
|
|
const checklistCount = items.filter(item => item.category === 'checklist').length;
|
|
|
|
const todoCountEl = document.getElementById('todoCount');
|
|
const calendarCountEl = document.getElementById('calendarCount');
|
|
const checklistCountEl = document.getElementById('checklistCount');
|
|
|
|
if (todoCountEl) todoCountEl.textContent = `${todoCount}개`;
|
|
if (calendarCountEl) calendarCountEl.textContent = `${calendarCount}개`;
|
|
if (checklistCountEl) checklistCountEl.textContent = `${checklistCount}개`;
|
|
} catch (error) {
|
|
console.error('항목 카운트 업데이트 실패:', error);
|
|
// 에러 시 0개로 표시
|
|
const todoCountEl = document.getElementById('todoCount');
|
|
const calendarCountEl = document.getElementById('calendarCount');
|
|
const checklistCountEl = document.getElementById('checklistCount');
|
|
|
|
if (todoCountEl) todoCountEl.textContent = '0개';
|
|
if (calendarCountEl) calendarCountEl.textContent = '0개';
|
|
if (checklistCountEl) checklistCountEl.textContent = '0개';
|
|
}
|
|
}
|
|
|
|
// 등록된 항목들 로드 (카테고리가 없는 미분류 항목들)
|
|
async function loadRegisteredItems() {
|
|
try {
|
|
// 무한 로딩 방지: 토큰이 없으면 API 요청하지 않음
|
|
const token = localStorage.getItem('authToken');
|
|
if (!token) {
|
|
console.log('토큰이 없어서 API 요청을 건너뜁니다.');
|
|
renderRegisteredItems([]);
|
|
return;
|
|
}
|
|
|
|
// API에서 모든 항목을 가져와서 카테고리가 없는 것만 필터링
|
|
const allItems = await TodoAPI.getTodos();
|
|
const unclassifiedItems = allItems.filter(item => !item.category || item.category === null);
|
|
renderRegisteredItems(unclassifiedItems);
|
|
} catch (error) {
|
|
console.error('등록된 항목 로드 실패:', error);
|
|
renderRegisteredItems([]);
|
|
}
|
|
}
|
|
|
|
// 등록된 항목들 렌더링
|
|
function renderRegisteredItems(items) {
|
|
const itemsList = document.getElementById('itemsList');
|
|
const emptyState = document.getElementById('emptyState');
|
|
|
|
if (!itemsList || !emptyState) return;
|
|
|
|
if (!items || items.length === 0) {
|
|
itemsList.innerHTML = '';
|
|
emptyState.classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
emptyState.classList.add('hidden');
|
|
|
|
itemsList.innerHTML = items.map(item => `
|
|
<div class="p-6 hover:bg-gray-50 cursor-pointer transition-colors" onclick="showClassificationModal('${item.id}')">
|
|
<div class="flex items-start space-x-4">
|
|
<!-- 사진 (있는 경우) -->
|
|
${item.image_url ? `
|
|
<div class="flex-shrink-0">
|
|
<img src="${item.image_url}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- 내용 -->
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="text-gray-900 font-medium mb-2">${item.title}</h4>
|
|
<div class="flex items-center space-x-4 text-sm text-gray-500">
|
|
<span>
|
|
<i class="fas fa-clock mr-1"></i>등록: ${formatDate(item.created_at)}
|
|
</span>
|
|
${item.category ? `
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getCategoryColor(item.category)}">
|
|
${getCategoryText(item.category)}
|
|
</span>
|
|
` : `
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
미분류
|
|
</span>
|
|
`}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 분류 아이콘 -->
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-chevron-right text-gray-400"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// 분류 모달 표시
|
|
function showClassificationModal(itemId) {
|
|
console.log('분류 모달 표시:', itemId);
|
|
|
|
// 기존 항목 정보 가져오기
|
|
const item = todos.find(t => t.id == itemId) || { title: '', description: '' };
|
|
|
|
// 모달 HTML 생성
|
|
const modalHtml = `
|
|
<div id="classificationModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg p-6 w-full max-w-md mx-4">
|
|
<h3 class="text-lg font-semibold mb-4">항목 분류 및 편집</h3>
|
|
|
|
<!-- 제목 입력 -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">제목</label>
|
|
<input type="text" id="itemTitle" value="${item.title || ''}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder="할 일을 입력하세요">
|
|
</div>
|
|
|
|
<!-- 설명 입력 -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">설명 (선택사항)</label>
|
|
<textarea id="itemDescription" rows="3"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
|
placeholder="상세 설명을 입력하세요">${item.description || ''}</textarea>
|
|
</div>
|
|
|
|
<!-- 카테고리 선택 -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">분류 선택</label>
|
|
<div class="grid grid-cols-3 gap-2">
|
|
<button type="button" onclick="selectCategory('todo')"
|
|
class="category-btn p-3 border-2 border-gray-200 rounded-lg text-center hover:border-blue-500 transition-colors"
|
|
data-category="todo">
|
|
<i class="fas fa-calendar-day text-blue-500 text-xl mb-1"></i>
|
|
<div class="text-sm font-medium">Todo</div>
|
|
<div class="text-xs text-gray-500">시작 날짜</div>
|
|
</button>
|
|
<button type="button" onclick="selectCategory('calendar')"
|
|
class="category-btn p-3 border-2 border-gray-200 rounded-lg text-center hover:border-orange-500 transition-colors"
|
|
data-category="calendar">
|
|
<i class="fas fa-calendar-times text-orange-500 text-xl mb-1"></i>
|
|
<div class="text-sm font-medium">캘린더</div>
|
|
<div class="text-xs text-gray-500">마감 기한</div>
|
|
</button>
|
|
<button type="button" onclick="selectCategory('checklist')"
|
|
class="category-btn p-3 border-2 border-gray-200 rounded-lg text-center hover:border-green-500 transition-colors"
|
|
data-category="checklist">
|
|
<i class="fas fa-check-square text-green-500 text-xl mb-1"></i>
|
|
<div class="text-sm font-medium">체크리스트</div>
|
|
<div class="text-xs text-gray-500">기한 없음</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 날짜 선택 (카테고리에 따라 표시) -->
|
|
<div id="dateSection" class="mb-4 hidden">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2" id="dateLabel">날짜 선택</label>
|
|
<input type="date" id="itemDate"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
|
|
<!-- 우선순위 선택 -->
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">우선순위</label>
|
|
<select id="itemPriority" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="low">낮음</option>
|
|
<option value="medium" selected>보통</option>
|
|
<option value="high">높음</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 버튼 -->
|
|
<div class="flex space-x-3">
|
|
<button onclick="closeClassificationModal()"
|
|
class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">
|
|
취소
|
|
</button>
|
|
<button onclick="saveClassification('${itemId}')"
|
|
class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
|
확인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 모달을 body에 추가
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
// 현재 선택된 카테고리 변수 (기본값: todo)
|
|
window.selectedCategory = 'todo';
|
|
|
|
// 기본적으로 todo 카테고리 선택 상태로 표시
|
|
setTimeout(() => {
|
|
selectCategory('todo');
|
|
}, 100);
|
|
|
|
// 모달 외부 클릭 시 닫기
|
|
const modal = document.getElementById('classificationModal');
|
|
modal.addEventListener('click', (e) => {
|
|
if (e.target === modal) {
|
|
closeClassificationModal();
|
|
}
|
|
});
|
|
|
|
// ESC 키로 모달 닫기
|
|
const handleEscKey = (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeClassificationModal();
|
|
document.removeEventListener('keydown', handleEscKey);
|
|
}
|
|
};
|
|
document.addEventListener('keydown', handleEscKey);
|
|
}
|
|
|
|
// 카테고리 선택
|
|
function selectCategory(category) {
|
|
// 이전 선택 해제
|
|
document.querySelectorAll('.category-btn').forEach(btn => {
|
|
btn.classList.remove('border-blue-500', 'border-orange-500', 'border-green-500', 'bg-blue-50', 'bg-orange-50', 'bg-green-50');
|
|
btn.classList.add('border-gray-200');
|
|
});
|
|
|
|
// 새 선택 적용
|
|
const selectedBtn = document.querySelector(`[data-category="${category}"]`);
|
|
if (selectedBtn) {
|
|
selectedBtn.classList.remove('border-gray-200');
|
|
if (category === 'todo') {
|
|
selectedBtn.classList.add('border-blue-500', 'bg-blue-50');
|
|
} else if (category === 'calendar') {
|
|
selectedBtn.classList.add('border-orange-500', 'bg-orange-50');
|
|
} else if (category === 'checklist') {
|
|
selectedBtn.classList.add('border-green-500', 'bg-green-50');
|
|
}
|
|
}
|
|
|
|
// 날짜 섹션 표시/숨김
|
|
const dateSection = document.getElementById('dateSection');
|
|
const dateLabel = document.getElementById('dateLabel');
|
|
|
|
if (category === 'checklist') {
|
|
dateSection.classList.add('hidden');
|
|
} else {
|
|
dateSection.classList.remove('hidden');
|
|
if (category === 'todo') {
|
|
dateLabel.textContent = '시작 날짜';
|
|
} else if (category === 'calendar') {
|
|
dateLabel.textContent = '마감 날짜';
|
|
}
|
|
}
|
|
|
|
window.selectedCategory = category;
|
|
}
|
|
|
|
// 분류 모달 닫기
|
|
function closeClassificationModal() {
|
|
const modal = document.getElementById('classificationModal');
|
|
if (modal) {
|
|
modal.remove();
|
|
}
|
|
window.selectedCategory = null;
|
|
}
|
|
|
|
// 분류 저장
|
|
async function saveClassification(itemId) {
|
|
const title = document.getElementById('itemTitle').value.trim();
|
|
const description = document.getElementById('itemDescription').value.trim();
|
|
const priority = document.getElementById('itemPriority').value;
|
|
const date = document.getElementById('itemDate').value;
|
|
|
|
if (!title) {
|
|
alert('제목을 입력해주세요.');
|
|
return;
|
|
}
|
|
|
|
if (!window.selectedCategory) {
|
|
alert('분류를 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// API 호출하여 항목 업데이트
|
|
const updateData = {
|
|
title: title,
|
|
description: description,
|
|
category: window.selectedCategory,
|
|
priority: priority
|
|
};
|
|
|
|
// 날짜 설정 (체크리스트가 아닌 경우)
|
|
if (window.selectedCategory !== 'checklist' && date) {
|
|
updateData.due_date = date + 'T09:00:00Z'; // 기본 시간 설정
|
|
}
|
|
|
|
await TodoAPI.updateTodo(itemId, updateData);
|
|
|
|
// 성공 메시지
|
|
showToast(`항목이 ${getCategoryText(window.selectedCategory)}(으)로 분류되었습니다.`, 'success');
|
|
|
|
// 모달 닫기
|
|
closeClassificationModal();
|
|
|
|
// todo가 아닌 다른 카테고리로 변경한 경우에만 페이지 이동
|
|
if (window.selectedCategory !== 'todo') {
|
|
setTimeout(() => {
|
|
goToPage(window.selectedCategory);
|
|
}, 1000); // 토스트 메시지를 보여준 후 이동
|
|
} else {
|
|
// todo 카테고리인 경우 인덱스 페이지에서 목록만 새로고침
|
|
loadRegisteredItems();
|
|
updateItemCounts();
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('분류 저장 실패:', error);
|
|
alert('분류 저장에 실패했습니다.');
|
|
}
|
|
}
|
|
|
|
// 항목 분류 (기존 함수 - 호환성 유지)
|
|
function classifyItem(itemId, category) {
|
|
console.log('항목 분류:', itemId, category);
|
|
goToPage(category);
|
|
}
|
|
|
|
// 분류별 색상
|
|
function getCategoryColor(category) {
|
|
const colors = {
|
|
'todo': 'bg-blue-100 text-blue-800',
|
|
'calendar': 'bg-orange-100 text-orange-800',
|
|
'checklist': 'bg-green-100 text-green-800'
|
|
};
|
|
return colors[category] || 'bg-gray-100 text-gray-800';
|
|
}
|
|
|
|
// 분류별 텍스트
|
|
function getCategoryText(category) {
|
|
const texts = {
|
|
'todo': 'Todo',
|
|
'calendar': '캘린더',
|
|
'checklist': '체크리스트'
|
|
};
|
|
return texts[category] || '미분류';
|
|
}
|
|
|
|
// 전역으로 사용 가능하도록 export
|
|
window.loadTodos = loadTodos;
|
|
window.openCamera = openCamera;
|
|
window.openGallery = openGallery;
|
|
window.removePhoto = removePhoto;
|
|
window.clearForm = clearForm;
|
|
window.toggleTodo = toggleTodo;
|
|
window.deleteTodo = deleteTodo;
|
|
window.editTodo = editTodo;
|
|
window.filterTodos = filterTodos;
|
|
window.goToPage = goToPage;
|
|
window.goToDashboard = goToDashboard;
|
|
window.goToClassify = goToClassify;
|
|
window.showClassificationModal = showClassificationModal;
|
|
window.updateItemCounts = updateItemCounts;
|