Files
Todo-Project/frontend/classify.html
hyungi f80995c1ec feat: 체크리스트 이미지 미리보기 기능 구현
- 체크리스트 섹션에 이미지 썸네일 미리보기 추가 (16x16)
- 대시보드 상단 체크리스트 카드에 이미지 미리보기 기능 추가
- 이미지 클릭 시 전체 화면 모달로 확대 보기
- 백엔드 image_url 컬럼을 TEXT 타입으로 변경하여 Base64 이미지 지원
- 파일 업로드를 이미지만 지원하도록 단순화 (file_url, file_name 제거)
- 422 validation 오류 해결 및 상세 로깅 추가
- 체크리스트 렌더링 누락 문제 해결
2025-09-23 07:49:54 +09:00

615 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>INDEX - Todo Project</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
}
body {
background-color: var(--gray-50);
}
.btn-primary {
background-color: var(--primary);
color: white;
transition: all 0.2s;
}
.btn-primary:hover {
background-color: var(--primary-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* 분류 카드 스타일 */
.classify-card {
background: white;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.classify-card:hover {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.classify-card.selected {
border-color: var(--primary);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.2);
}
/* 분류 버튼 스타일 */
.classify-btn {
padding: 12px 24px;
border-radius: 12px;
font-weight: 600;
transition: all 0.2s;
border: 2px solid transparent;
}
.classify-btn.todo {
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
color: #1e40af;
border-color: #3b82f6;
}
.classify-btn.todo:hover {
background: linear-gradient(135deg, #bfdbfe, #93c5fd);
transform: scale(1.05);
}
.classify-btn.calendar {
background: linear-gradient(135deg, #fef3c7, #fde68a);
color: #92400e;
border-color: #f59e0b;
}
.classify-btn.calendar:hover {
background: linear-gradient(135deg, #fde68a, #fcd34d);
transform: scale(1.05);
}
.classify-btn.checklist {
background: linear-gradient(135deg, #d1fae5, #a7f3d0);
color: #065f46;
border-color: #10b981;
}
.classify-btn.checklist:hover {
background: linear-gradient(135deg, #a7f3d0, #6ee7b7);
transform: scale(1.05);
}
/* 스마트 제안 스타일 */
.smart-suggestion {
background: linear-gradient(135deg, #f3e8ff, #e9d5ff);
border: 2px solid #8b5cf6;
border-radius: 12px;
padding: 12px;
margin: 12px 0;
}
/* 태그 스타일 */
.tag {
display: inline-flex;
align-items: center;
padding: 4px 12px;
background: #f1f5f9;
color: #475569;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
margin: 2px;
cursor: pointer;
transition: all 0.2s;
}
.tag:hover {
background: #e2e8f0;
}
.tag.selected {
background: var(--primary);
color: white;
}
/* 애니메이션 */
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-up {
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* 모바일 최적화 */
@media (max-width: 768px) {
.classify-btn {
padding: 10px 16px;
font-size: 14px;
}
.classify-card {
margin: 8px 0;
}
}
</style>
</head>
<body>
<div class="min-h-screen">
<!-- 헤더 -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<button onclick="goBack()" class="mr-4 text-gray-500 hover:text-gray-700">
<i class="fas fa-arrow-left text-xl"></i>
</button>
<i class="fas fa-inbox text-2xl text-purple-500 mr-3"></i>
<h1 class="text-xl font-semibold text-gray-800">INDEX</h1>
<span class="ml-3 px-2 py-1 bg-red-100 text-red-800 text-sm rounded-full" id="pendingCount">0</span>
</div>
<div class="flex items-center space-x-4">
<button onclick="goToDashboard()" class="text-blue-600 hover:text-blue-800 font-medium">
<i class="fas fa-chart-line mr-1"></i>대시보드
</button>
<button onclick="selectAll()" class="text-gray-600 hover:text-gray-800 text-sm">
<i class="fas fa-check-square mr-1"></i>전체선택
</button>
<span class="text-sm text-gray-600" id="currentUser"></span>
<button onclick="logout()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
</div>
</div>
</header>
<!-- 메인 컨텐츠 -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 상단 통계 및 필터 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<!-- 통계 카드들 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center">
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
<i class="fas fa-inbox text-purple-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm text-gray-600">분류 대기</p>
<p class="text-2xl font-bold text-gray-900" id="totalPending">0</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center">
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<i class="fas fa-calendar-day text-blue-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm text-gray-600">Todo 이동</p>
<p class="text-2xl font-bold text-gray-900" id="todoMoved">0</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center">
<div class="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center">
<i class="fas fa-calendar-times text-orange-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm text-gray-600">캘린더 이동</p>
<p class="text-2xl font-bold text-gray-900" id="calendarMoved">0</p>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center">
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
<i class="fas fa-check-square text-green-600 text-xl"></i>
</div>
<div class="ml-4">
<p class="text-sm text-gray-600">체크리스트 이동</p>
<p class="text-2xl font-bold text-gray-900" id="checklistMoved">0</p>
</div>
</div>
</div>
</div>
<!-- 필터 및 정렬 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div class="flex flex-wrap gap-2">
<button onclick="filterItems('all')" class="filter-btn active px-4 py-2 rounded-lg text-sm font-medium">전체</button>
<button onclick="filterItems('upload')" class="filter-btn px-4 py-2 rounded-lg text-sm font-medium">업로드</button>
<button onclick="filterItems('mail')" class="filter-btn px-4 py-2 rounded-lg text-sm font-medium">메일</button>
<button onclick="filterItems('suggested')" class="filter-btn px-4 py-2 rounded-lg text-sm font-medium">제안 있음</button>
</div>
<div class="flex items-center space-x-4">
<select id="sortBy" class="border border-gray-300 rounded-lg px-3 py-2 text-sm">
<option value="newest">최신순</option>
<option value="oldest">오래된순</option>
<option value="suggested">제안순</option>
</select>
<button onclick="batchClassify()" class="btn-primary px-4 py-2 rounded-lg text-sm" disabled id="batchBtn">
<i class="fas fa-layer-group mr-1"></i>일괄 분류
</button>
</div>
</div>
</div>
<!-- 분류 대기 항목들 -->
<div class="space-y-4" id="classifyItems">
<!-- 항목들이 여기에 동적으로 추가됩니다 -->
</div>
<!-- 빈 상태 -->
<div id="emptyState" class="hidden text-center py-16">
<i class="fas fa-inbox text-6xl text-gray-300 mb-4"></i>
<h3 class="text-xl font-semibold text-gray-600 mb-2">분류할 항목이 없습니다</h3>
<p class="text-gray-500 mb-6">새로운 항목을 업로드하거나 메일을 받으면 여기에 표시됩니다.</p>
<button onclick="goToDashboard()" class="btn-primary px-6 py-3 rounded-lg">
<i class="fas fa-plus mr-2"></i>새 항목 추가
</button>
</div>
</main>
</div>
<!-- JavaScript -->
<script src="static/js/api.js?v=2"></script>
<script src="static/js/auth.js?v=2"></script>
<script>
let pendingItems = [];
let selectedItems = [];
let currentFilter = 'all';
// 페이지 초기화
document.addEventListener('DOMContentLoaded', () => {
checkAuthStatus();
loadPendingItems();
updateStats();
});
// 분류 대기 항목 로드
function loadPendingItems() {
// 분류되지 않은 항목들을 API에서 가져와야 함
// 현재는 빈 배열로 설정 (분류 기능 미구현)
pendingItems = [];
renderItems();
}
// 항목들 렌더링
function renderItems() {
const container = document.getElementById('classifyItems');
const emptyState = document.getElementById('emptyState');
// 필터링
let filteredItems = pendingItems;
if (currentFilter !== 'all') {
filteredItems = pendingItems.filter(item => {
if (currentFilter === 'suggested') return item.suggested;
return item.type === currentFilter;
});
}
if (filteredItems.length === 0) {
container.innerHTML = '';
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
container.innerHTML = filteredItems.map(item => `
<div class="classify-card p-6 ${selectedItems.includes(item.id) ? 'selected' : ''}" data-id="${item.id}">
<div class="flex items-start space-x-4">
<!-- 선택 체크박스 -->
<div class="flex-shrink-0 mt-1">
<input type="checkbox" class="w-5 h-5 text-blue-600 rounded"
${selectedItems.includes(item.id) ? 'checked' : ''}
onchange="toggleSelection(${item.id})">
</div>
<!-- 타입 아이콘 -->
<div class="flex-shrink-0">
<div class="w-12 h-12 rounded-lg flex items-center justify-center ${item.type === 'upload' ? 'bg-blue-100' : 'bg-green-100'}">
<i class="fas ${item.type === 'upload' ? 'fa-camera text-blue-600' : 'fa-envelope text-green-600'} text-xl"></i>
</div>
</div>
<!-- 사진 (있는 경우) -->
${item.photo ? `
<div class="flex-shrink-0">
<img src="${item.photo}" class="w-20 h-20 object-cover rounded-lg" alt="첨부 사진">
</div>
` : ''}
<!-- 내용 -->
<div class="flex-1 min-w-0">
<h4 class="text-lg font-medium text-gray-900 mb-2">${item.content}</h4>
<!-- 메타 정보 -->
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-3">
<span>
<i class="fas fa-clock mr-1"></i>${formatDate(item.created_at)}
</span>
<span>
<i class="fas fa-source mr-1"></i>${item.source}
</span>
${item.sender ? `
<span>
<i class="fas fa-user mr-1"></i>${item.sender}
</span>
` : ''}
</div>
<!-- 태그 -->
<div class="flex flex-wrap gap-1 mb-3">
${item.tags.map(tag => `<span class="tag">#${tag}</span>`).join('')}
</div>
<!-- 스마트 제안 -->
${item.suggested ? `
<div class="smart-suggestion">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-magic text-purple-600 mr-2"></i>
<span class="text-sm font-medium text-purple-800">
AI 제안: <strong>${getSuggestionText(item.suggested)}</strong>
</span>
<span class="ml-2 text-xs text-purple-600">(${Math.round(item.confidence * 100)}% 확신)</span>
</div>
<button onclick="acceptSuggestion('${item.id}', '${item.suggested}')"
class="text-xs bg-purple-600 text-white px-3 py-1 rounded-full hover:bg-purple-700">
적용
</button>
</div>
</div>
` : ''}
</div>
</div>
<!-- 분류 버튼들 -->
<div class="mt-6 flex flex-wrap gap-3 justify-center">
<button onclick="classifyItem('${item.id}', 'todo')" class="classify-btn todo">
<i class="fas fa-calendar-day mr-2"></i>Todo
<div class="text-xs opacity-75">시작 날짜</div>
</button>
<button onclick="classifyItem('${item.id}', 'calendar')" class="classify-btn calendar">
<i class="fas fa-calendar-times mr-2"></i>캘린더
<div class="text-xs opacity-75">마감 기한</div>
</button>
<button onclick="classifyItem('${item.id}', 'checklist')" class="classify-btn checklist">
<i class="fas fa-check-square mr-2"></i>체크리스트
<div class="text-xs opacity-75">기한 없음</div>
</button>
</div>
</div>
`).join('');
// 애니메이션 적용
container.querySelectorAll('.classify-card').forEach((card, index) => {
card.style.animationDelay = `${index * 0.1}s`;
card.classList.add('fade-in');
});
}
// 항목 선택 토글
function toggleSelection(id) {
const index = selectedItems.indexOf(id);
if (index > -1) {
selectedItems.splice(index, 1);
} else {
selectedItems.push(id);
}
updateBatchButton();
renderItems();
}
// 전체 선택
function selectAll() {
if (selectedItems.length === pendingItems.length) {
selectedItems = [];
} else {
selectedItems = pendingItems.map(item => item.id);
}
updateBatchButton();
renderItems();
}
// 일괄 분류 버튼 업데이트
function updateBatchButton() {
const batchBtn = document.getElementById('batchBtn');
if (selectedItems.length > 0) {
batchBtn.disabled = false;
batchBtn.textContent = `${selectedItems.length}개 일괄 분류`;
} else {
batchBtn.disabled = true;
batchBtn.innerHTML = '<i class="fas fa-layer-group mr-1"></i>일괄 분류';
}
}
// 개별 항목 분류
function classifyItem(id, category) {
const item = pendingItems.find(item => item.id === id);
if (!item) return;
// 애니메이션 효과
const card = document.querySelector(`[data-id="${id}"]`);
card.style.transform = 'scale(0.95)';
card.style.opacity = '0.7';
setTimeout(() => {
// 항목 제거
pendingItems = pendingItems.filter(item => item.id !== id);
selectedItems = selectedItems.filter(itemId => itemId !== id);
// UI 업데이트
renderItems();
updateStats();
updateBatchButton();
// 성공 메시지
showToast(`"${item.content}"이(가) ${getSuggestionText(category)}(으)로 이동되었습니다.`, 'success');
// TODO: API 호출하여 실제 분류 처리
console.log(`항목 ${id}을(를) ${category}로 분류`);
}, 300);
}
// 제안 수락
function acceptSuggestion(id, category) {
classifyItem(id, category);
}
// 일괄 분류
function batchClassify() {
if (selectedItems.length === 0) return;
// 일괄 분류 모달 또는 드롭다운 표시
const category = prompt(`선택된 ${selectedItems.length}개 항목을 어디로 분류하시겠습니까?\n1. Todo\n2. 캘린더\n3. 체크리스트\n\n번호를 입력하세요:`);
const categories = { '1': 'todo', '2': 'calendar', '3': 'checklist' };
const selectedCategory = categories[category];
if (selectedCategory) {
selectedItems.forEach(id => {
setTimeout(() => classifyItem(id, selectedCategory), Math.random() * 500);
});
}
}
// 필터링
function filterItems(filter) {
currentFilter = filter;
// 필터 버튼 활성화 상태 업데이트
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active', 'bg-blue-600', 'text-white');
btn.classList.add('text-gray-600', 'bg-gray-100');
});
event.target.classList.add('active', 'bg-blue-600', 'text-white');
event.target.classList.remove('text-gray-600', 'bg-gray-100');
renderItems();
}
// 통계 업데이트
function updateStats() {
document.getElementById('totalPending').textContent = pendingItems.length;
document.getElementById('pendingCount').textContent = pendingItems.length;
// TODO: 실제 이동된 항목 수 계산
document.getElementById('todoMoved').textContent = '5';
document.getElementById('calendarMoved').textContent = '3';
document.getElementById('checklistMoved').textContent = '7';
}
// 유틸리티 함수들
function getSuggestionText(category) {
const texts = {
'todo': 'Todo',
'calendar': '캘린더',
'checklist': '체크리스트'
};
return texts[category] || '미분류';
}
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 diffHours = Math.floor(diffTime / (1000 * 60 * 60));
if (diffHours < 1) return '방금 전';
if (diffHours < 24) return `${diffHours}시간 전`;
return date.toLocaleDateString('ko-KR', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function showToast(message, type = 'info') {
// 간단한 토스트 메시지 (실제로는 더 예쁜 토스트 UI 구현)
console.log(`[${type.toUpperCase()}] ${message}`);
// 임시 알림
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
type === 'success' ? 'bg-green-500' : 'bg-blue-500'
}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// 네비게이션 함수들
function goBack() {
window.location.href = 'index.html';
}
function goToDashboard() {
window.location.href = 'dashboard.html';
}
// 전역 함수 등록
window.toggleSelection = toggleSelection;
window.selectAll = selectAll;
window.classifyItem = classifyItem;
window.acceptSuggestion = acceptSuggestion;
window.batchClassify = batchClassify;
window.filterItems = filterItems;
window.goBack = goBack;
window.goToDashboard = goToDashboard;
</script>
</body>
</html>