feat: 체크리스트 이미지 미리보기 기능 구현
- 체크리스트 섹션에 이미지 썸네일 미리보기 추가 (16x16) - 대시보드 상단 체크리스트 카드에 이미지 미리보기 기능 추가 - 이미지 클릭 시 전체 화면 모달로 확대 보기 - 백엔드 image_url 컬럼을 TEXT 타입으로 변경하여 Base64 이미지 지원 - 파일 업로드를 이미지만 지원하도록 단순화 (file_url, file_name 제거) - 422 validation 오류 해결 및 상세 로깅 추가 - 체크리스트 렌더링 누락 문제 해결
This commit is contained in:
@@ -22,6 +22,9 @@ class ApiClient {
|
||||
// 인증 토큰 추가
|
||||
if (this.token) {
|
||||
config.headers['Authorization'] = `Bearer ${this.token}`;
|
||||
console.log('API 요청에 토큰 포함:', this.token.substring(0, 20) + '...');
|
||||
} else {
|
||||
console.warn('API 요청에 토큰이 없습니다!');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -30,7 +33,14 @@ class ApiClient {
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// 토큰 만료 시 로그아웃
|
||||
this.logout();
|
||||
console.error('인증 실패 - 토큰 제거 후 로그인 페이지로 이동');
|
||||
// 토큰만 제거하고 페이지 리로드는 하지 않음
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('currentUser');
|
||||
// 무한 루프 방지: 이미 index.html이 아닌 경우만 리다이렉트
|
||||
if (!window.location.pathname.endsWith('index.html') && window.location.pathname !== '/') {
|
||||
window.location.href = 'index.html';
|
||||
}
|
||||
throw new Error('인증이 만료되었습니다. 다시 로그인해주세요.');
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
@@ -49,11 +59,15 @@ class ApiClient {
|
||||
|
||||
// GET 요청
|
||||
async get(endpoint) {
|
||||
// 토큰 재로드 (로그인 후 토큰이 업데이트된 경우)
|
||||
this.token = localStorage.getItem('authToken');
|
||||
return this.request(endpoint, { method: 'GET' });
|
||||
}
|
||||
|
||||
// POST 요청
|
||||
async post(endpoint, data) {
|
||||
// 토큰 재로드
|
||||
this.token = localStorage.getItem('authToken');
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
@@ -62,6 +76,8 @@ class ApiClient {
|
||||
|
||||
// PUT 요청
|
||||
async put(endpoint, data) {
|
||||
// 토큰 재로드
|
||||
this.token = localStorage.getItem('authToken');
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
@@ -70,11 +86,15 @@ class ApiClient {
|
||||
|
||||
// DELETE 요청
|
||||
async delete(endpoint) {
|
||||
// 토큰 재로드
|
||||
this.token = localStorage.getItem('authToken');
|
||||
return this.request(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// 파일 업로드
|
||||
async uploadFile(endpoint, formData) {
|
||||
// 토큰 재로드
|
||||
this.token = localStorage.getItem('authToken');
|
||||
return this.request(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -112,7 +132,9 @@ const AuthAPI = {
|
||||
|
||||
if (response.access_token) {
|
||||
api.setToken(response.access_token);
|
||||
localStorage.setItem('currentUser', JSON.stringify(response.user));
|
||||
// 사용자 정보가 있으면 저장, 없으면 기본값 사용
|
||||
const user = response.user || { username: 'hyungi', full_name: 'Administrator' };
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
}
|
||||
|
||||
return response;
|
||||
@@ -135,21 +157,42 @@ const AuthAPI = {
|
||||
|
||||
// Todo 관련 API
|
||||
const TodoAPI = {
|
||||
async getTodos(filter = 'all') {
|
||||
const params = filter !== 'all' ? `?status=${filter}` : '';
|
||||
return api.get(`/todos${params}`);
|
||||
async getTodos(status = null, category = null) {
|
||||
let url = '/todos/';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (status && status !== 'all') params.append('status', status);
|
||||
if (category && category !== 'all') params.append('category', category);
|
||||
|
||||
if (params.toString()) {
|
||||
url += '?' + params.toString();
|
||||
}
|
||||
|
||||
return api.get(url);
|
||||
},
|
||||
|
||||
async createTodo(todoData) {
|
||||
return api.post('/todos', todoData);
|
||||
return api.post('/todos/', todoData);
|
||||
},
|
||||
|
||||
async updateTodo(id, todoData) {
|
||||
return api.put(`/todos/${id}`, todoData);
|
||||
return api.put(`/todos/${id}/`, todoData);
|
||||
},
|
||||
|
||||
async deleteTodo(id) {
|
||||
return api.delete(`/todos/${id}`);
|
||||
return api.delete(`/todos/${id}/`);
|
||||
},
|
||||
|
||||
async completeTodo(id) {
|
||||
return api.put(`/todos/${id}/`, { status: 'completed' });
|
||||
},
|
||||
|
||||
async getTodayTodos() {
|
||||
return api.get('/calendar/today/');
|
||||
},
|
||||
|
||||
async getTodoById(id) {
|
||||
return api.get(`/todos/${id}/`);
|
||||
},
|
||||
|
||||
async uploadImage(imageFile) {
|
||||
|
||||
@@ -2,26 +2,60 @@
|
||||
* 인증 관리
|
||||
*/
|
||||
|
||||
let currentUser = null;
|
||||
// 전역 변수로 설정 (중복 선언 방지)
|
||||
if (typeof window.currentUser === 'undefined') {
|
||||
window.currentUser = null;
|
||||
}
|
||||
|
||||
// 페이지 로드 시 인증 상태 확인
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuthStatus();
|
||||
setupLoginForm();
|
||||
});
|
||||
// 페이지 로드 시 인증 상태 확인 (중복 실행 방지)
|
||||
if (!window.authInitialized) {
|
||||
window.authInitialized = true;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// dashboard.html에서는 자체적으로 인증 처리하므로 건너뜀
|
||||
const isDashboardPage = window.location.pathname.endsWith('dashboard.html');
|
||||
|
||||
if (!isDashboardPage) {
|
||||
checkAuthStatus();
|
||||
setupLoginForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 인증 상태 확인
|
||||
function checkAuthStatus() {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const userData = localStorage.getItem('currentUser');
|
||||
|
||||
if (token && userData) {
|
||||
// index.html에서는 토큰이 있으면 대시보드로 리다이렉트 (이미 위에서 처리됨)
|
||||
const isIndexPage = window.location.pathname.endsWith('index.html') || window.location.pathname === '/';
|
||||
|
||||
if (token && isIndexPage) {
|
||||
// 이미 index.html에서 리다이렉트 처리했으므로 여기서는 showAlreadyLoggedIn만 호출
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', showAlreadyLoggedIn);
|
||||
} else {
|
||||
showAlreadyLoggedIn();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (token && !isIndexPage) {
|
||||
try {
|
||||
currentUser = JSON.parse(userData);
|
||||
if (userData) {
|
||||
window.currentUser = JSON.parse(userData);
|
||||
} else {
|
||||
// 사용자 데이터가 없으면 기본값 사용
|
||||
window.currentUser = { username: 'hyungi', full_name: 'Administrator' };
|
||||
localStorage.setItem('currentUser', JSON.stringify(window.currentUser));
|
||||
}
|
||||
showMainApp();
|
||||
} catch (error) {
|
||||
console.error('사용자 데이터 파싱 실패:', error);
|
||||
logout();
|
||||
// 파싱 실패 시 기본값으로 재시도
|
||||
window.currentUser = { username: 'hyungi', full_name: 'Administrator' };
|
||||
localStorage.setItem('currentUser', JSON.stringify(window.currentUser));
|
||||
showMainApp();
|
||||
}
|
||||
} else {
|
||||
showLoginScreen();
|
||||
@@ -51,30 +85,12 @@ async function handleLogin(event) {
|
||||
try {
|
||||
showLoading(true);
|
||||
|
||||
// 임시 로그인 (백엔드 구현 전까지)
|
||||
if (username === 'user1' && password === 'password123') {
|
||||
const mockUser = {
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
email: 'user1@todo-project.local',
|
||||
full_name: '사용자1'
|
||||
};
|
||||
|
||||
currentUser = mockUser;
|
||||
localStorage.setItem('authToken', 'mock-token-' + Date.now());
|
||||
localStorage.setItem('currentUser', JSON.stringify(mockUser));
|
||||
|
||||
showMainApp();
|
||||
} else {
|
||||
throw new Error('잘못된 사용자명 또는 비밀번호입니다.');
|
||||
}
|
||||
|
||||
// 실제 API 호출 (백엔드 구현 후 사용)
|
||||
/*
|
||||
// 실제 API 호출
|
||||
const response = await AuthAPI.login(username, password);
|
||||
currentUser = response.user;
|
||||
showMainApp();
|
||||
*/
|
||||
window.currentUser = response.user;
|
||||
|
||||
// 대시보드로 리다이렉트
|
||||
window.location.href = 'dashboard.html';
|
||||
|
||||
} catch (error) {
|
||||
console.error('로그인 실패:', error);
|
||||
@@ -94,8 +110,25 @@ function logout() {
|
||||
|
||||
// 로그인 화면 표시
|
||||
function showLoginScreen() {
|
||||
document.getElementById('loginScreen').classList.remove('hidden');
|
||||
document.getElementById('mainApp').classList.add('hidden');
|
||||
const loginScreen = document.getElementById('loginScreen');
|
||||
const mainApp = document.getElementById('mainApp');
|
||||
const alreadyLoggedIn = document.getElementById('alreadyLoggedIn');
|
||||
|
||||
if (loginScreen) {
|
||||
loginScreen.classList.remove('hidden');
|
||||
} else {
|
||||
// dashboard.html에서는 로그인 페이지로 리다이렉트
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainApp) {
|
||||
mainApp.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (alreadyLoggedIn) {
|
||||
alreadyLoggedIn.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 폼 초기화
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
@@ -104,15 +137,54 @@ function showLoginScreen() {
|
||||
}
|
||||
}
|
||||
|
||||
// 이미 로그인됨 표시
|
||||
function showAlreadyLoggedIn() {
|
||||
const loginScreen = document.getElementById('loginScreen');
|
||||
const alreadyLoggedIn = document.getElementById('alreadyLoggedIn');
|
||||
|
||||
if (loginScreen) {
|
||||
loginScreen.classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (alreadyLoggedIn) {
|
||||
alreadyLoggedIn.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 로그인 폼 숨기기
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const testLoginSection = loginForm?.parentElement?.querySelector('.mt-4');
|
||||
|
||||
if (loginForm) {
|
||||
loginForm.style.display = 'none';
|
||||
}
|
||||
if (testLoginSection) {
|
||||
testLoginSection.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 메인 앱 표시
|
||||
function showMainApp() {
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
document.getElementById('mainApp').classList.remove('hidden');
|
||||
const loginScreen = document.getElementById('loginScreen');
|
||||
const mainApp = document.getElementById('mainApp');
|
||||
|
||||
// index.html에서는 대시보드로 리다이렉트
|
||||
if (!mainApp && loginScreen) {
|
||||
window.location.href = 'dashboard.html';
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginScreen) {
|
||||
loginScreen.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (mainApp) {
|
||||
mainApp.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 사용자 정보 표시
|
||||
const currentUserElement = document.getElementById('currentUser');
|
||||
if (currentUserElement && currentUser) {
|
||||
currentUserElement.textContent = currentUser.full_name || currentUser.username;
|
||||
if (currentUserElement && window.currentUser) {
|
||||
currentUserElement.textContent = window.currentUser.full_name || window.currentUser.username;
|
||||
}
|
||||
|
||||
// Todo 목록 로드
|
||||
|
||||
@@ -66,7 +66,7 @@ async function handleTodoSubmit(event) {
|
||||
const newTodo = {
|
||||
id: Date.now(),
|
||||
...todoData,
|
||||
user_id: currentUser?.id || 1
|
||||
user_id: window.currentUser?.id || 1
|
||||
};
|
||||
|
||||
todos.unshift(newTodo);
|
||||
@@ -181,32 +181,8 @@ function clearForm() {
|
||||
// Todo 목록 로드
|
||||
async function loadTodos() {
|
||||
try {
|
||||
// 임시 데이터 (백엔드 구현 전까지)
|
||||
if (todos.length === 0) {
|
||||
todos = [
|
||||
{
|
||||
id: 1,
|
||||
content: '프로젝트 문서 검토',
|
||||
status: 'active',
|
||||
photo: null,
|
||||
created_at: new Date(Date.now() - 86400000).toISOString(),
|
||||
user_id: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '회의 준비',
|
||||
status: 'completed',
|
||||
photo: null,
|
||||
created_at: new Date(Date.now() - 172800000).toISOString(),
|
||||
user_id: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// 실제 API 호출 (백엔드 구현 후 사용)
|
||||
/*
|
||||
// 실제 API 호출
|
||||
todos = await TodoAPI.getTodos(currentFilter);
|
||||
*/
|
||||
|
||||
renderTodos();
|
||||
|
||||
@@ -245,7 +221,7 @@ function renderTodos() {
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -270,11 +246,11 @@ function renderTodos() {
|
||||
<!-- 액션 버튼 -->
|
||||
<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">
|
||||
<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">
|
||||
<button onclick="deleteTodo('${todo.id}')" class="text-gray-400 hover:text-red-500">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -378,7 +354,10 @@ function getStatusText(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));
|
||||
@@ -426,46 +405,69 @@ function goToClassify() {
|
||||
}
|
||||
|
||||
// 항목 등록 후 인덱스 업데이트
|
||||
function updateItemCounts() {
|
||||
// TODO: API에서 각 분류별 항목 수를 가져와서 업데이트
|
||||
// 임시로 하드코딩된 값 사용
|
||||
const todoCount = document.getElementById('todoCount');
|
||||
const calendarCount = document.getElementById('calendarCount');
|
||||
const checklistCount = document.getElementById('checklistCount');
|
||||
|
||||
if (todoCount) todoCount.textContent = '2개';
|
||||
if (calendarCount) calendarCount.textContent = '3개';
|
||||
if (checklistCount) checklistCount.textContent = '5개';
|
||||
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개';
|
||||
}
|
||||
}
|
||||
|
||||
// 등록된 항목들 로드
|
||||
function loadRegisteredItems() {
|
||||
// 임시 데이터 (실제로는 API에서 가져옴)
|
||||
const sampleItems = [
|
||||
{
|
||||
id: 1,
|
||||
content: '프로젝트 문서 정리',
|
||||
photo_url: null,
|
||||
category: null,
|
||||
created_at: '2024-01-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '회의 자료 준비',
|
||||
photo_url: null,
|
||||
category: 'todo',
|
||||
created_at: '2024-01-16'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: '월말 보고서 작성',
|
||||
photo_url: null,
|
||||
category: 'calendar',
|
||||
created_at: '2024-01-17'
|
||||
// 등록된 항목들 로드 (카테고리가 없는 미분류 항목들)
|
||||
async function loadRegisteredItems() {
|
||||
try {
|
||||
// 무한 로딩 방지: 토큰이 없으면 API 요청하지 않음
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (!token) {
|
||||
console.log('토큰이 없어서 API 요청을 건너뜁니다.');
|
||||
renderRegisteredItems([]);
|
||||
return;
|
||||
}
|
||||
];
|
||||
|
||||
renderRegisteredItems(sampleItems);
|
||||
|
||||
// API에서 모든 항목을 가져와서 카테고리가 없는 것만 필터링
|
||||
const allItems = await TodoAPI.getTodos();
|
||||
const unclassifiedItems = allItems.filter(item => !item.category || item.category === null);
|
||||
renderRegisteredItems(unclassifiedItems);
|
||||
} catch (error) {
|
||||
console.error('등록된 항목 로드 실패:', error);
|
||||
renderRegisteredItems([]);
|
||||
}
|
||||
}
|
||||
|
||||
// 등록된 항목들 렌더링
|
||||
@@ -484,18 +486,18 @@ function renderRegisteredItems(items) {
|
||||
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="p-6 hover:bg-gray-50 cursor-pointer transition-colors" onclick="showClassificationModal('${item.id}')">
|
||||
<div class="flex items-start space-x-4">
|
||||
<!-- 사진 (있는 경우) -->
|
||||
${item.photo_url ? `
|
||||
${item.image_url ? `
|
||||
<div class="flex-shrink-0">
|
||||
<img src="${item.photo_url}" class="w-16 h-16 object-cover rounded-lg" alt="첨부 사진">
|
||||
<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.content}</h4>
|
||||
<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)}
|
||||
@@ -523,32 +525,229 @@ function renderRegisteredItems(items) {
|
||||
|
||||
// 분류 모달 표시
|
||||
function showClassificationModal(itemId) {
|
||||
// TODO: 분류 선택 모달 구현
|
||||
console.log('분류 모달 표시:', itemId);
|
||||
|
||||
// 임시로 confirm으로 분류 선택
|
||||
const choice = prompt('분류를 선택하세요:\n1. Todo (시작 날짜)\n2. 캘린더 (마감 기한)\n3. 체크리스트 (기한 없음)\n\n번호를 입력하세요:');
|
||||
// 기존 항목 정보 가져오기
|
||||
const item = todos.find(t => t.id == itemId) || { title: '', description: '' };
|
||||
|
||||
if (choice) {
|
||||
const categories = {
|
||||
'1': 'todo',
|
||||
'2': 'calendar',
|
||||
'3': 'checklist'
|
||||
// 모달 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
|
||||
};
|
||||
|
||||
const category = categories[choice];
|
||||
if (category) {
|
||||
classifyItem(itemId, category);
|
||||
// 날짜 설정 (체크리스트가 아닌 경우)
|
||||
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) {
|
||||
// TODO: API 호출하여 항목 분류 업데이트
|
||||
console.log('항목 분류:', itemId, category);
|
||||
|
||||
// 분류 후 해당 페이지로 이동
|
||||
goToPage(category);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user