Files
M-Project/frontend/chart.html
hyungi 1339e5dded feat: 작업보고서 시스템 완성
- 일일 공수 입력 기능
- 부적합 사항 등록 (이미지 선택사항)
- 날짜별 부적합 조회 (시간순 나열)
- 목록 관리 (인라인 편집, 작업시간 확인 버튼)
- 보고서 생성 (총 공수/부적합 시간 분리)
- JWT 인증 및 권한 관리
- Docker 기반 배포 환경 구성
2025-09-17 10:41:25 +09:00

418 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M-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">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--sky-50: #f0f9ff;
--sky-100: #e0f2fe;
--sky-200: #bae6fd;
--sky-300: #7dd3fc;
--sky-400: #38bdf8;
--sky-500: #0ea5e9;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
}
body {
background: linear-gradient(to bottom, #ffffff, #f0f9ff);
min-height: 100vh;
}
.glass {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.stat-card {
background: white;
border-left: 4px solid var(--sky-400);
transition: all 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
.gallery-item {
cursor: pointer;
transition: all 0.2s;
}
.gallery-item:hover {
transform: scale(1.05);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<!-- 헤더 -->
<header class="glass sticky top-0 z-50 shadow-sm">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<h1 class="text-xl font-bold text-gray-800">
<i class="fas fa-chart-bar text-sky-500 mr-2"></i>차트
</h1>
<div class="flex items-center gap-4">
<a href="index.html" class="text-sky-600 hover:text-sky-700">
<i class="fas fa-camera text-xl"></i>
</a>
<span class="text-gray-600 text-sm" id="userName"></span>
</div>
</div>
</header>
<!-- 메인 컨텐츠 -->
<main class="container mx-auto px-4 py-6 max-w-6xl">
<!-- 통계 카드 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div class="stat-card p-4 rounded-xl shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">전체 사진</p>
<p class="text-2xl font-bold text-gray-800" id="totalCount">0</p>
</div>
<i class="fas fa-images text-3xl text-sky-400"></i>
</div>
</div>
<div class="stat-card p-4 rounded-xl shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">오늘</p>
<p class="text-2xl font-bold text-gray-800" id="todayCount">0</p>
</div>
<i class="fas fa-calendar-day text-3xl text-sky-400"></i>
</div>
</div>
<div class="stat-card p-4 rounded-xl shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">이번 주</p>
<p class="text-2xl font-bold text-gray-800" id="weekCount">0</p>
</div>
<i class="fas fa-calendar-week text-3xl text-sky-400"></i>
</div>
</div>
<div class="stat-card p-4 rounded-xl shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">가장 많은</p>
<p class="text-lg font-bold text-gray-800" id="topCategory">-</p>
</div>
<i class="fas fa-trophy text-3xl text-sky-400"></i>
</div>
</div>
</div>
<!-- 차트 영역 -->
<div class="grid md:grid-cols-2 gap-6 mb-8">
<!-- 카테고리별 차트 -->
<div class="glass p-6 rounded-xl shadow-lg">
<h3 class="text-lg font-semibold text-gray-800 mb-4">카테고리별 분포</h3>
<canvas id="categoryChart" width="400" height="300"></canvas>
</div>
<!-- 일별 추이 차트 -->
<div class="glass p-6 rounded-xl shadow-lg">
<h3 class="text-lg font-semibold text-gray-800 mb-4">최근 7일 추이</h3>
<canvas id="trendChart" width="400" height="300"></canvas>
</div>
</div>
<!-- 갤러리 -->
<div class="glass p-6 rounded-xl shadow-lg">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-800">전체 갤러리</h3>
<select id="categoryFilter" class="px-4 py-2 rounded-lg border border-gray-300 focus:border-sky-400 focus:outline-none">
<option value="all">전체 카테고리</option>
<option value="food">음식</option>
<option value="place">장소</option>
<option value="people">사람</option>
<option value="object">물건</option>
<option value="event">행사</option>
<option value="etc">기타</option>
</select>
</div>
<div id="gallery" class="grid grid-cols-3 md:grid-cols-6 gap-3">
<!-- 갤러리 아이템들이 여기에 표시됩니다 -->
</div>
</div>
</main>
<!-- 이미지 모달 -->
<div id="imageModal" class="modal" onclick="closeModal()">
<div class="bg-white rounded-xl p-4 max-w-2xl mx-4" onclick="event.stopPropagation()">
<img id="modalImage" class="w-full rounded-lg mb-4">
<div class="flex justify-between items-start">
<div>
<p class="font-semibold text-lg" id="modalDescription"></p>
<p class="text-sm text-gray-600 mt-1" id="modalInfo"></p>
</div>
<button onclick="closeModal()" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times text-xl"></i>
</button>
</div>
</div>
</div>
<script>
let allData = [];
let currentUser = null;
// 초기화
window.onload = () => {
// 로그인 체크
const params = new URLSearchParams(window.location.search);
currentUser = params.get('user');
if (!currentUser) {
// 로컬스토리지에서 최근 사용자 확인
const recentUser = localStorage.getItem('m-project-recent-user');
if (recentUser) {
currentUser = recentUser;
} else {
window.location.href = 'index.html';
return;
}
}
document.getElementById('userName').textContent = currentUser;
loadData();
};
// 데이터 로드
function loadData() {
const savedData = JSON.parse(localStorage.getItem('m-project-data') || '[]');
allData = savedData.filter(item => item.user === currentUser);
updateStats();
createCharts();
displayGallery();
}
// 통계 업데이트
function updateStats() {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
document.getElementById('totalCount').textContent = allData.length;
// 오늘 카운트
const todayCount = allData.filter(item => {
const itemDate = new Date(item.timestamp);
return itemDate >= today;
}).length;
document.getElementById('todayCount').textContent = todayCount;
// 이번 주 카운트
const weekCount = allData.filter(item => {
const itemDate = new Date(item.timestamp);
return itemDate >= weekAgo;
}).length;
document.getElementById('weekCount').textContent = weekCount;
// 가장 많은 카테고리
const categoryCounts = {};
allData.forEach(item => {
categoryCounts[item.category] = (categoryCounts[item.category] || 0) + 1;
});
const categoryNames = {
food: '음식',
place: '장소',
people: '사람',
object: '물건',
event: '행사',
etc: '기타'
};
if (Object.keys(categoryCounts).length > 0) {
const topCat = Object.entries(categoryCounts)
.sort((a, b) => b[1] - a[1])[0][0];
document.getElementById('topCategory').textContent = categoryNames[topCat];
}
}
// 차트 생성
function createCharts() {
// 카테고리별 차트
const categoryCounts = {};
allData.forEach(item => {
categoryCounts[item.category] = (categoryCounts[item.category] || 0) + 1;
});
const categoryNames = {
food: '음식',
place: '장소',
people: '사람',
object: '물건',
event: '행사',
etc: '기타'
};
const categoryCtx = document.getElementById('categoryChart').getContext('2d');
new Chart(categoryCtx, {
type: 'doughnut',
data: {
labels: Object.keys(categoryCounts).map(key => categoryNames[key]),
datasets: [{
data: Object.values(categoryCounts),
backgroundColor: [
'#0ea5e9',
'#38bdf8',
'#7dd3fc',
'#bae6fd',
'#e0f2fe',
'#f0f9ff'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
// 일별 추이 차트
const today = new Date();
const dates = [];
const counts = [];
for (let i = 6; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
date.setHours(0, 0, 0, 0);
const nextDate = new Date(date);
nextDate.setDate(nextDate.getDate() + 1);
const count = allData.filter(item => {
const itemDate = new Date(item.timestamp);
return itemDate >= date && itemDate < nextDate;
}).length;
dates.push(date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }));
counts.push(count);
}
const trendCtx = document.getElementById('trendChart').getContext('2d');
new Chart(trendCtx, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: '업로드 수',
data: counts,
borderColor: '#0ea5e9',
backgroundColor: 'rgba(14, 165, 233, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
// 갤러리 표시
function displayGallery(filter = 'all') {
const gallery = document.getElementById('gallery');
gallery.innerHTML = '';
const filteredData = filter === 'all'
? allData
: allData.filter(item => item.category === filter);
// 최신순으로 정렬
filteredData.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
filteredData.forEach(item => {
const div = document.createElement('div');
div.className = 'gallery-item';
div.innerHTML = `<img src="${item.image}" class="w-full h-24 object-cover rounded-lg shadow-sm">`;
div.onclick = () => showModal(item);
gallery.appendChild(div);
});
if (filteredData.length === 0) {
gallery.innerHTML = '<p class="col-span-full text-center text-gray-500 py-8">표시할 이미지가 없습니다.</p>';
}
}
// 필터 변경
document.getElementById('categoryFilter').addEventListener('change', (e) => {
displayGallery(e.target.value);
});
// 모달 표시
function showModal(item) {
const categoryNames = {
food: '음식',
place: '장소',
people: '사람',
object: '물건',
event: '행사',
etc: '기타'
};
document.getElementById('modalImage').src = item.image;
document.getElementById('modalDescription').textContent = item.description;
document.getElementById('modalInfo').textContent =
`${categoryNames[item.category]}${new Date(item.timestamp).toLocaleString('ko-KR')}`;
document.getElementById('imageModal').classList.add('show');
}
// 모달 닫기
function closeModal() {
document.getElementById('imageModal').classList.remove('show');
}
</script>
</body>
</html>