merge: 원격 변경사항과 병합 및 충돌 해결
- 임시 파일들 정리 (chart.html, daily-work.html 등) - localStorage 기반 프로젝트별 보고서 필터링 유지 - 공통 헤더 및 인증 시스템 적용
This commit is contained in:
417
chart.html
417
chart.html
@@ -1,417 +0,0 @@
|
|||||||
<!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>
|
|
||||||
378
daily-work.html
378
daily-work.html
@@ -1,378 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>일일 공수 입력</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;
|
|
||||||
--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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background-color: var(--success);
|
|
||||||
color: white;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover {
|
|
||||||
background-color: #059669;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field {
|
|
||||||
border: 1px solid var(--gray-300);
|
|
||||||
background: white;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field:focus {
|
|
||||||
border-color: var(--primary);
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-card {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.work-card:hover {
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card {
|
|
||||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="min-h-screen bg-gray-50">
|
|
||||||
<!-- 헤더 -->
|
|
||||||
<header class="bg-white shadow-sm sticky top-0 z-50">
|
|
||||||
<div class="container mx-auto px-4 py-3">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<h1 class="text-xl font-bold text-gray-800">
|
|
||||||
<i class="fas fa-calendar-check text-blue-500 mr-2"></i>일일 공수 입력
|
|
||||||
</h1>
|
|
||||||
<a href="index.html" class="text-gray-600 hover:text-gray-800">
|
|
||||||
<i class="fas fa-arrow-left mr-2"></i>돌아가기
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- 메인 컨텐츠 -->
|
|
||||||
<main class="container mx-auto px-4 py-6 max-w-2xl">
|
|
||||||
<!-- 입력 카드 -->
|
|
||||||
<div class="work-card p-6 mb-6">
|
|
||||||
<h2 class="text-lg font-semibold text-gray-800 mb-6">
|
|
||||||
<i class="fas fa-edit text-blue-500 mr-2"></i>공수 입력
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<form id="dailyWorkForm" class="space-y-6">
|
|
||||||
<!-- 날짜 선택 -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
<i class="fas fa-calendar mr-1"></i>날짜
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
id="workDate"
|
|
||||||
class="input-field w-full px-4 py-3 rounded-lg text-lg"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 인원 입력 -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
<i class="fas fa-users mr-1"></i>작업 인원
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="workerCount"
|
|
||||||
min="1"
|
|
||||||
class="input-field w-full px-4 py-3 rounded-lg text-lg"
|
|
||||||
placeholder="예: 5"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<p class="text-sm text-gray-500 mt-1">기본 근무시간: 8시간/인</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 잔업 섹션 -->
|
|
||||||
<div class="border-t pt-4">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<label class="text-sm font-medium text-gray-700">
|
|
||||||
<i class="fas fa-clock mr-1"></i>잔업 여부
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
id="overtimeToggle"
|
|
||||||
class="px-4 py-2 rounded-lg border border-gray-300 hover:bg-gray-50 transition-colors"
|
|
||||||
onclick="toggleOvertime()"
|
|
||||||
>
|
|
||||||
<i class="fas fa-plus mr-2"></i>잔업 추가
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 잔업 입력 영역 -->
|
|
||||||
<div id="overtimeSection" class="hidden space-y-3 bg-gray-50 p-4 rounded-lg">
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">잔업 인원</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="overtimeWorkers"
|
|
||||||
min="0"
|
|
||||||
class="input-field w-full px-3 py-2 rounded"
|
|
||||||
placeholder="명"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">잔업 시간</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="overtimeHours"
|
|
||||||
min="0"
|
|
||||||
step="0.5"
|
|
||||||
class="input-field w-full px-3 py-2 rounded"
|
|
||||||
placeholder="시간"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 총 공수 표시 -->
|
|
||||||
<div class="bg-blue-50 rounded-lg p-4">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-gray-700 font-medium">예상 총 공수</span>
|
|
||||||
<span class="text-2xl font-bold text-blue-600" id="totalHours">0시간</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 저장 버튼 -->
|
|
||||||
<button type="submit" class="btn-primary w-full py-3 rounded-lg font-medium text-lg">
|
|
||||||
<i class="fas fa-save mr-2"></i>저장하기
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 최근 입력 내역 -->
|
|
||||||
<div class="work-card p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
<i class="fas fa-history text-gray-500 mr-2"></i>최근 입력 내역
|
|
||||||
</h3>
|
|
||||||
<div id="recentEntries" class="space-y-3">
|
|
||||||
<!-- 최근 입력 내역이 여기에 표시됩니다 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 오늘 날짜로 초기화
|
|
||||||
document.getElementById('workDate').valueAsDate = new Date();
|
|
||||||
|
|
||||||
// 잔업 토글
|
|
||||||
function toggleOvertime() {
|
|
||||||
const section = document.getElementById('overtimeSection');
|
|
||||||
const button = document.getElementById('overtimeToggle');
|
|
||||||
|
|
||||||
if (section.classList.contains('hidden')) {
|
|
||||||
section.classList.remove('hidden');
|
|
||||||
button.innerHTML = '<i class="fas fa-minus mr-2"></i>잔업 취소';
|
|
||||||
button.classList.add('bg-gray-100');
|
|
||||||
} else {
|
|
||||||
section.classList.add('hidden');
|
|
||||||
button.innerHTML = '<i class="fas fa-plus mr-2"></i>잔업 추가';
|
|
||||||
button.classList.remove('bg-gray-100');
|
|
||||||
// 잔업 입력값 초기화
|
|
||||||
document.getElementById('overtimeWorkers').value = '';
|
|
||||||
document.getElementById('overtimeHours').value = '';
|
|
||||||
}
|
|
||||||
calculateTotal();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 총 공수 계산
|
|
||||||
function calculateTotal() {
|
|
||||||
const workerCount = parseInt(document.getElementById('workerCount').value) || 0;
|
|
||||||
const regularHours = workerCount * 8; // 기본 8시간
|
|
||||||
|
|
||||||
let overtimeTotal = 0;
|
|
||||||
if (!document.getElementById('overtimeSection').classList.contains('hidden')) {
|
|
||||||
const overtimeWorkers = parseInt(document.getElementById('overtimeWorkers').value) || 0;
|
|
||||||
const overtimeHours = parseFloat(document.getElementById('overtimeHours').value) || 0;
|
|
||||||
overtimeTotal = overtimeWorkers * overtimeHours;
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = regularHours + overtimeTotal;
|
|
||||||
document.getElementById('totalHours').textContent = `${total}시간`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 입력값 변경 시 총 공수 재계산
|
|
||||||
document.getElementById('workerCount').addEventListener('input', calculateTotal);
|
|
||||||
document.getElementById('overtimeWorkers').addEventListener('input', calculateTotal);
|
|
||||||
document.getElementById('overtimeHours').addEventListener('input', calculateTotal);
|
|
||||||
|
|
||||||
// 폼 제출
|
|
||||||
document.getElementById('dailyWorkForm').addEventListener('submit', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const workDate = document.getElementById('workDate').value;
|
|
||||||
const workerCount = parseInt(document.getElementById('workerCount').value);
|
|
||||||
const regularHours = workerCount * 8;
|
|
||||||
|
|
||||||
let overtimeWorkers = 0;
|
|
||||||
let overtimeHours = 0;
|
|
||||||
let overtimeTotal = 0;
|
|
||||||
|
|
||||||
if (!document.getElementById('overtimeSection').classList.contains('hidden')) {
|
|
||||||
overtimeWorkers = parseInt(document.getElementById('overtimeWorkers').value) || 0;
|
|
||||||
overtimeHours = parseFloat(document.getElementById('overtimeHours').value) || 0;
|
|
||||||
overtimeTotal = overtimeWorkers * overtimeHours;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalHours = regularHours + overtimeTotal;
|
|
||||||
|
|
||||||
// 데이터 객체 생성
|
|
||||||
const workData = {
|
|
||||||
id: Date.now(),
|
|
||||||
date: workDate,
|
|
||||||
workerCount: workerCount,
|
|
||||||
regularHours: regularHours,
|
|
||||||
overtimeWorkers: overtimeWorkers,
|
|
||||||
overtimeHours: overtimeHours,
|
|
||||||
overtimeTotal: overtimeTotal,
|
|
||||||
totalHours: totalHours,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 로컬 스토리지에 저장
|
|
||||||
saveWorkData(workData);
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
showSuccessMessage();
|
|
||||||
|
|
||||||
// 폼 초기화
|
|
||||||
resetForm();
|
|
||||||
|
|
||||||
// 최근 내역 갱신
|
|
||||||
displayRecentEntries();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 데이터 저장
|
|
||||||
function saveWorkData(data) {
|
|
||||||
const savedData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
|
||||||
|
|
||||||
// 같은 날짜 데이터가 있으면 업데이트
|
|
||||||
const existingIndex = savedData.findIndex(item => item.date === data.date);
|
|
||||||
if (existingIndex > -1) {
|
|
||||||
savedData[existingIndex] = data;
|
|
||||||
} else {
|
|
||||||
savedData.push(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('daily-work-data', JSON.stringify(savedData));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
function showSuccessMessage() {
|
|
||||||
const button = document.querySelector('button[type="submit"]');
|
|
||||||
const originalHTML = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check-circle mr-2"></i>저장 완료!';
|
|
||||||
button.classList.remove('btn-primary');
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
button.innerHTML = originalHTML;
|
|
||||||
button.classList.remove('btn-success');
|
|
||||||
button.classList.add('btn-primary');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 폼 초기화
|
|
||||||
function resetForm() {
|
|
||||||
document.getElementById('workerCount').value = '';
|
|
||||||
document.getElementById('overtimeWorkers').value = '';
|
|
||||||
document.getElementById('overtimeHours').value = '';
|
|
||||||
document.getElementById('overtimeSection').classList.add('hidden');
|
|
||||||
document.getElementById('overtimeToggle').innerHTML = '<i class="fas fa-plus mr-2"></i>잔업 추가';
|
|
||||||
document.getElementById('overtimeToggle').classList.remove('bg-gray-100');
|
|
||||||
document.getElementById('totalHours').textContent = '0시간';
|
|
||||||
|
|
||||||
// 날짜는 오늘로 유지
|
|
||||||
document.getElementById('workDate').valueAsDate = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 최근 입력 내역 표시
|
|
||||||
function displayRecentEntries() {
|
|
||||||
const container = document.getElementById('recentEntries');
|
|
||||||
const savedData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
|
||||||
|
|
||||||
// 최근 7개만 표시
|
|
||||||
const recentData = savedData.slice(-7).reverse();
|
|
||||||
|
|
||||||
if (recentData.length === 0) {
|
|
||||||
container.innerHTML = '<p class="text-gray-500 text-center py-4">입력된 내역이 없습니다.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = recentData.map(item => {
|
|
||||||
const date = new Date(item.date);
|
|
||||||
const dateStr = date.toLocaleDateString('ko-KR', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
weekday: 'short'
|
|
||||||
});
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
|
|
||||||
<div>
|
|
||||||
<p class="font-medium text-gray-800">${dateStr}</p>
|
|
||||||
<p class="text-sm text-gray-600">
|
|
||||||
인원: ${item.workerCount}명
|
|
||||||
${item.overtimeTotal > 0 ? `• 잔업: ${item.overtimeWorkers}명 × ${item.overtimeHours}시간` : ''}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<p class="text-lg font-bold text-blue-600">${item.totalHours}시간</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 페이지 로드 시 최근 내역 표시
|
|
||||||
displayRecentEntries();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -305,10 +305,14 @@
|
|||||||
// 사용자 목록 로드
|
// 사용자 목록 로드
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
try {
|
try {
|
||||||
|
// 백엔드 API에서 사용자 목록 로드
|
||||||
users = await AuthAPI.getUsers();
|
users = await AuthAPI.getUsers();
|
||||||
displayUsers();
|
displayUsers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('사용자 목록 로드 실패:', error);
|
console.error('사용자 목록 로드 실패:', error);
|
||||||
|
// API 실패 시 빈 배열로 초기화
|
||||||
|
users = [];
|
||||||
|
displayUsers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,18 +343,53 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${user.username !== 'hyungi' ? `
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onclick="deleteUser('${user.username}')"
|
onclick="resetPassword('${user.username}')"
|
||||||
class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm"
|
class="px-3 py-1 bg-yellow-500 text-white rounded hover:bg-yellow-600 transition-colors text-sm"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash mr-1"></i>삭제
|
<i class="fas fa-key mr-1"></i>비밀번호 초기화
|
||||||
</button>
|
</button>
|
||||||
` : ''}
|
${user.username !== 'hyungi' ? `
|
||||||
|
<button
|
||||||
|
onclick="deleteUser('${user.username}')"
|
||||||
|
class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash mr-1"></i>삭제
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 비밀번호 초기화
|
||||||
|
async function resetPassword(username) {
|
||||||
|
if (!confirm(`${username} 사용자의 비밀번호를 "000000"으로 초기화하시겠습니까?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 사용자 ID 찾기
|
||||||
|
const user = users.find(u => u.username === username);
|
||||||
|
if (!user) {
|
||||||
|
alert('사용자를 찾을 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 백엔드 API로 비밀번호 초기화
|
||||||
|
await AuthAPI.resetPassword(user.id, '000000');
|
||||||
|
|
||||||
|
alert(`${username} 사용자의 비밀번호가 "000000"으로 초기화되었습니다.`);
|
||||||
|
|
||||||
|
// 목록 새로고침
|
||||||
|
await loadUsers();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert('비밀번호 초기화에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 사용자 삭제
|
// 사용자 삭제
|
||||||
async function deleteUser(username) {
|
async function deleteUser(username) {
|
||||||
if (!confirm(`정말 ${username} 사용자를 삭제하시겠습니까?`)) {
|
if (!confirm(`정말 ${username} 사용자를 삭제하시겠습니까?`)) {
|
||||||
|
|||||||
@@ -1,417 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>API 프로젝트 생성 - 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="/static/js/api.js?v=20250917"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50">
|
|
||||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
|
||||||
<i class="fas fa-plus text-green-500 mr-2"></i>API 프로젝트 생성
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
||||||
<h3 class="font-semibold text-green-800 mb-2">🎯 작업 내용</h3>
|
|
||||||
<p class="text-green-700 text-sm">
|
|
||||||
백엔드 API를 통해 TKR-25009R M Project를 생성합니다.<br>
|
|
||||||
데이터베이스에 실제 프로젝트 레코드가 생성됩니다.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="status" class="space-y-2">
|
|
||||||
<!-- 상태 메시지가 여기에 표시됩니다 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="createBtn"
|
|
||||||
onclick="createProject()"
|
|
||||||
class="w-full bg-green-500 text-white py-3 px-4 rounded-lg hover:bg-green-600 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<i class="fas fa-plus mr-2"></i>TKR-25009R 프로젝트 생성
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="fix-api-data.html" class="block text-center text-orange-600 hover:text-orange-800 mt-4">
|
|
||||||
<i class="fas fa-wrench mr-1"></i>API 데이터 수정 도구로 이동
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="debug-data.html" class="block text-center text-blue-600 hover:text-blue-800 mt-2">
|
|
||||||
<i class="fas fa-bug mr-1"></i>디버그 도구로 이동
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let currentUser = null;
|
|
||||||
|
|
||||||
// 페이지 로드 시 사용자 확인
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const user = TokenManager.getUser();
|
|
||||||
if (!user) {
|
|
||||||
alert('로그인이 필요합니다.');
|
|
||||||
window.location.href = 'index.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUser = user;
|
|
||||||
|
|
||||||
if (currentUser.role !== 'admin') {
|
|
||||||
alert('관리자만 접근 가능합니다.');
|
|
||||||
window.location.href = 'index.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addStatus('✅ 관리자 권한 확인됨', 'text-green-600');
|
|
||||||
});
|
|
||||||
|
|
||||||
function addStatus(message, className = 'text-gray-600') {
|
|
||||||
const statusDiv = document.getElementById('status');
|
|
||||||
const p = document.createElement('p');
|
|
||||||
p.className = `text-sm ${className}`;
|
|
||||||
p.innerHTML = `<i class="fas fa-info-circle mr-2"></i>${message}`;
|
|
||||||
statusDiv.appendChild(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createProject() {
|
|
||||||
const btn = document.getElementById('createBtn');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>생성 중...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 기존 프로젝트 확인
|
|
||||||
addStatus('기존 프로젝트 확인 중...');
|
|
||||||
|
|
||||||
let existingProjects = [];
|
|
||||||
try {
|
|
||||||
existingProjects = await ProjectsAPI.getAll();
|
|
||||||
addStatus(`기존 프로젝트 ${existingProjects.length}개 발견`, 'text-blue-600');
|
|
||||||
} catch (error) {
|
|
||||||
addStatus('기존 프로젝트 조회 실패, 새로 생성합니다.', 'text-yellow-600');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TKR-25009R 프로젝트가 이미 있는지 확인
|
|
||||||
const existingMProject = existingProjects.find(p => p.job_no === 'TKR-25009R');
|
|
||||||
if (existingMProject) {
|
|
||||||
addStatus(`✅ TKR-25009R 프로젝트가 이미 존재합니다 (ID: ${existingMProject.id})`, 'text-green-600');
|
|
||||||
|
|
||||||
// localStorage 프로젝트 ID 업데이트
|
|
||||||
updateLocalStorageProjectId(existingMProject.id);
|
|
||||||
|
|
||||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>이미 존재함';
|
|
||||||
btn.className = 'w-full bg-blue-500 text-white py-3 px-4 rounded-lg font-medium';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 새 프로젝트 생성
|
|
||||||
addStatus('TKR-25009R 프로젝트 생성 중...');
|
|
||||||
|
|
||||||
const projectData = {
|
|
||||||
job_no: 'TKR-25009R',
|
|
||||||
project_name: 'M Project'
|
|
||||||
};
|
|
||||||
|
|
||||||
const newProject = await ProjectsAPI.create(projectData);
|
|
||||||
addStatus(`✅ 프로젝트 생성 완료! ID: ${newProject.id}`, 'text-green-600');
|
|
||||||
|
|
||||||
// 3. localStorage 프로젝트 ID 업데이트
|
|
||||||
updateLocalStorageProjectId(newProject.id);
|
|
||||||
|
|
||||||
// 완료
|
|
||||||
addStatus('🎉 프로젝트 생성 및 동기화 완료!', 'text-green-600 font-bold');
|
|
||||||
addStatus('이제 API 데이터 수정 도구를 실행하세요.', 'text-blue-600');
|
|
||||||
|
|
||||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>생성 완료';
|
|
||||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
addStatus(`❌ 오류 발생: ${error.message}`, 'text-red-600');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateLocalStorageProjectId(apiProjectId) {
|
|
||||||
try {
|
|
||||||
// localStorage 프로젝트 데이터 업데이트
|
|
||||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
|
||||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
|
||||||
|
|
||||||
if (mProject) {
|
|
||||||
const oldId = mProject.id;
|
|
||||||
mProject.id = apiProjectId;
|
|
||||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
|
||||||
addStatus(`localStorage 프로젝트 ID 업데이트: ${oldId} → ${apiProjectId}`, 'text-blue-600');
|
|
||||||
|
|
||||||
// 일일 공수 데이터도 업데이트
|
|
||||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
|
||||||
let updatedCount = 0;
|
|
||||||
|
|
||||||
dailyWorkData.forEach(dayData => {
|
|
||||||
if (dayData.projects) {
|
|
||||||
dayData.projects.forEach(project => {
|
|
||||||
if (project.projectId == oldId || project.projectId == 1) {
|
|
||||||
project.projectId = apiProjectId;
|
|
||||||
project.projectName = 'TKR-25009R - M Project';
|
|
||||||
updatedCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
|
|
||||||
addStatus(`일일 공수 데이터 ${updatedCount}개 업데이트`, 'text-blue-600');
|
|
||||||
|
|
||||||
// localStorage 부적합 사항도 업데이트
|
|
||||||
const localIssues = JSON.parse(localStorage.getItem('work-report-issues') || '[]');
|
|
||||||
let issueUpdatedCount = 0;
|
|
||||||
|
|
||||||
localIssues.forEach(issue => {
|
|
||||||
if (issue.projectId == oldId || issue.projectId == 1 || issue.project_id == oldId || issue.project_id == 1) {
|
|
||||||
issue.projectId = apiProjectId;
|
|
||||||
issue.project_id = apiProjectId;
|
|
||||||
issue.projectName = 'TKR-25009R - M Project';
|
|
||||||
issue.project_name = 'TKR-25009R - M Project';
|
|
||||||
issueUpdatedCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem('work-report-issues', JSON.stringify(localIssues));
|
|
||||||
addStatus(`localStorage 부적합 사항 ${issueUpdatedCount}개 업데이트`, 'text-blue-600');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
addStatus(`localStorage 업데이트 실패: ${error.message}`, 'text-red-600');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
<!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="/static/js/api.js?v=20250917"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50">
|
|
||||||
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
|
||||||
<i class="fas fa-bug text-red-500 mr-2"></i>데이터 디버그
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="space-y-6">
|
|
||||||
<button
|
|
||||||
onclick="debugData()"
|
|
||||||
class="w-full bg-blue-500 text-white py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<i class="fas fa-search mr-2"></i>데이터 상태 확인
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div id="results" class="space-y-4">
|
|
||||||
<!-- 결과가 여기에 표시됩니다 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
async function debugData() {
|
|
||||||
const resultsDiv = document.getElementById('results');
|
|
||||||
resultsDiv.innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin text-2xl"></i></div>';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 프로젝트 데이터 확인
|
|
||||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
|
||||||
|
|
||||||
// 2. 부적합 사항 데이터 확인 (API)
|
|
||||||
let apiIssues = [];
|
|
||||||
try {
|
|
||||||
apiIssues = await IssuesAPI.getAll();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('API 조회 실패:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 부적합 사항 데이터 확인 (localStorage)
|
|
||||||
const localIssues = JSON.parse(localStorage.getItem('work-report-issues') || '[]');
|
|
||||||
|
|
||||||
// 4. 일일 공수 데이터 확인
|
|
||||||
const dailyWork = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
|
||||||
|
|
||||||
// 결과 표시
|
|
||||||
resultsDiv.innerHTML = `
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- 프로젝트 데이터 -->
|
|
||||||
<div class="bg-blue-50 p-4 rounded-lg">
|
|
||||||
<h3 class="font-bold text-blue-800 mb-3">📁 프로젝트 데이터 (${projects.length}개)</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
${projects.map(p => `
|
|
||||||
<div class="bg-white p-3 rounded border">
|
|
||||||
<div class="font-medium">ID: ${p.id}</div>
|
|
||||||
<div>Job No: ${p.jobNo}</div>
|
|
||||||
<div>이름: ${p.projectName}</div>
|
|
||||||
<div>활성: ${p.isActive ? '✅' : '❌'}</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API 부적합 사항 -->
|
|
||||||
<div class="bg-green-50 p-4 rounded-lg">
|
|
||||||
<h3 class="font-bold text-green-800 mb-3">🔗 API 부적합 사항 (${apiIssues.length}개)</h3>
|
|
||||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
|
||||||
${apiIssues.map(issue => `
|
|
||||||
<div class="bg-white p-3 rounded border text-sm">
|
|
||||||
<div><strong>ID:</strong> ${issue.id}</div>
|
|
||||||
<div><strong>project_id:</strong> ${issue.project_id || 'null'}</div>
|
|
||||||
<div><strong>project_name:</strong> ${issue.project_name || 'null'}</div>
|
|
||||||
<div><strong>설명:</strong> ${issue.description?.substring(0, 50)}...</div>
|
|
||||||
<div><strong>카테고리:</strong> ${issue.category}</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- localStorage 부적합 사항 -->
|
|
||||||
<div class="bg-yellow-50 p-4 rounded-lg">
|
|
||||||
<h3 class="font-bold text-yellow-800 mb-3">💾 localStorage 부적합 사항 (${localIssues.length}개)</h3>
|
|
||||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
|
||||||
${localIssues.map(issue => `
|
|
||||||
<div class="bg-white p-3 rounded border text-sm">
|
|
||||||
<div><strong>ID:</strong> ${issue.id}</div>
|
|
||||||
<div><strong>project_id:</strong> ${issue.project_id || 'null'}</div>
|
|
||||||
<div><strong>projectId:</strong> ${issue.projectId || 'null'}</div>
|
|
||||||
<div><strong>project_name:</strong> ${issue.project_name || 'null'}</div>
|
|
||||||
<div><strong>projectName:</strong> ${issue.projectName || 'null'}</div>
|
|
||||||
<div><strong>설명:</strong> ${issue.description?.substring(0, 50)}...</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 일일 공수 데이터 -->
|
|
||||||
<div class="bg-purple-50 p-4 rounded-lg">
|
|
||||||
<h3 class="font-bold text-purple-800 mb-3">⏰ 일일 공수 데이터 (${dailyWork.length}개)</h3>
|
|
||||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
|
||||||
${dailyWork.map(day => `
|
|
||||||
<div class="bg-white p-3 rounded border text-sm">
|
|
||||||
<div><strong>날짜:</strong> ${day.date}</div>
|
|
||||||
<div><strong>총 시간:</strong> ${day.totalHours}시간</div>
|
|
||||||
<div><strong>프로젝트들:</strong></div>
|
|
||||||
${day.projects?.map(p => `
|
|
||||||
<div class="ml-4 text-xs">
|
|
||||||
- ID: ${p.projectId}, 이름: ${p.projectName}, 시간: ${p.hours}
|
|
||||||
</div>
|
|
||||||
`).join('') || '<div class="ml-4 text-xs text-red-500">프로젝트 데이터 없음</div>'}
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 필터링 테스트 -->
|
|
||||||
<div class="bg-red-50 p-4 rounded-lg">
|
|
||||||
<h3 class="font-bold text-red-800 mb-3">🔍 필터링 테스트</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
${projects.map(project => {
|
|
||||||
const matchingApiIssues = apiIssues.filter(issue => issue.project_id == project.id);
|
|
||||||
const matchingLocalIssues = localIssues.filter(issue => issue.project_id == project.id || issue.projectId == project.id);
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="bg-white p-3 rounded border">
|
|
||||||
<div class="font-medium">${project.jobNo} - ${project.projectName} (ID: ${project.id})</div>
|
|
||||||
<div class="text-sm">API 매칭: ${matchingApiIssues.length}개</div>
|
|
||||||
<div class="text-sm">localStorage 매칭: ${matchingLocalIssues.length}개</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
resultsDiv.innerHTML = `
|
|
||||||
<div class="bg-red-50 p-4 rounded-lg">
|
|
||||||
<h3 class="font-bold text-red-800 mb-3">❌ 오류 발생</h3>
|
|
||||||
<pre class="text-sm">${error.message}</pre>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>API 데이터 수정 - 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="/static/js/api.js?v=20250917"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50">
|
|
||||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
|
||||||
<i class="fas fa-wrench text-orange-500 mr-2"></i>API 데이터 수정
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
||||||
<h3 class="font-semibold text-yellow-800 mb-2">⚠️ 주의사항</h3>
|
|
||||||
<p class="text-yellow-700 text-sm">
|
|
||||||
이 작업은 API 데이터베이스의 모든 부적합 사항에 TKR-25009R 프로젝트 정보를 추가합니다.<br>
|
|
||||||
관리자(hyungi) 계정으로 로그인한 상태에서만 실행하세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="status" class="space-y-2">
|
|
||||||
<!-- 상태 메시지가 여기에 표시됩니다 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="fixBtn"
|
|
||||||
onclick="fixApiData()"
|
|
||||||
class="w-full bg-orange-500 text-white py-3 px-4 rounded-lg hover:bg-orange-600 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<i class="fas fa-tools mr-2"></i>API 데이터 수정 시작
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="debug-data.html" class="block text-center text-blue-600 hover:text-blue-800 mt-4">
|
|
||||||
<i class="fas fa-bug mr-1"></i>디버그 도구로 이동
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="index.html" class="block text-center text-gray-600 hover:text-gray-800 mt-2">
|
|
||||||
<i class="fas fa-arrow-left mr-1"></i>메인으로 돌아가기
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let currentUser = null;
|
|
||||||
|
|
||||||
// 페이지 로드 시 사용자 확인
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const user = TokenManager.getUser();
|
|
||||||
if (!user) {
|
|
||||||
alert('로그인이 필요합니다.');
|
|
||||||
window.location.href = 'index.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUser = user;
|
|
||||||
|
|
||||||
if (currentUser.role !== 'admin') {
|
|
||||||
alert('관리자만 접근 가능합니다.');
|
|
||||||
window.location.href = 'index.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addStatus('✅ 관리자 권한 확인됨', 'text-green-600');
|
|
||||||
});
|
|
||||||
|
|
||||||
function addStatus(message, className = 'text-gray-600') {
|
|
||||||
const statusDiv = document.getElementById('status');
|
|
||||||
const p = document.createElement('p');
|
|
||||||
p.className = `text-sm ${className}`;
|
|
||||||
p.innerHTML = `<i class="fas fa-info-circle mr-2"></i>${message}`;
|
|
||||||
statusDiv.appendChild(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fixApiData() {
|
|
||||||
const btn = document.getElementById('fixBtn');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>수정 중...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. TKR-25009R 프로젝트 찾기
|
|
||||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
|
||||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
|
||||||
|
|
||||||
if (!mProject) {
|
|
||||||
throw new Error('TKR-25009R 프로젝트를 찾을 수 없습니다. 먼저 마이그레이션을 실행하세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
addStatus(`TKR-25009R 프로젝트 발견 (ID: ${mProject.id})`, 'text-blue-600');
|
|
||||||
|
|
||||||
// 2. API에서 모든 부적합 사항 가져오기
|
|
||||||
addStatus('API에서 부적합 사항 조회 중...');
|
|
||||||
const issues = await IssuesAPI.getAll();
|
|
||||||
addStatus(`총 ${issues.length}개 부적합 사항 발견`, 'text-blue-600');
|
|
||||||
|
|
||||||
// 3. project_id가 null인 부적합 사항들 찾기
|
|
||||||
const issuesWithoutProject = issues.filter(issue => !issue.project_id);
|
|
||||||
addStatus(`프로젝트 정보가 없는 부적합 사항: ${issuesWithoutProject.length}개`, 'text-yellow-600');
|
|
||||||
|
|
||||||
if (issuesWithoutProject.length === 0) {
|
|
||||||
addStatus('✅ 모든 부적합 사항에 이미 프로젝트 정보가 있습니다.', 'text-green-600');
|
|
||||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>완료';
|
|
||||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 각 부적합 사항 업데이트
|
|
||||||
let successCount = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < issuesWithoutProject.length; i++) {
|
|
||||||
const issue = issuesWithoutProject[i];
|
|
||||||
|
|
||||||
try {
|
|
||||||
addStatus(`${i + 1}/${issuesWithoutProject.length}: 부적합 사항 ID ${issue.id} 업데이트 중...`);
|
|
||||||
|
|
||||||
await IssuesAPI.update(issue.id, {
|
|
||||||
project_id: mProject.id
|
|
||||||
});
|
|
||||||
|
|
||||||
successCount++;
|
|
||||||
|
|
||||||
// UI 업데이트를 위한 짧은 대기
|
|
||||||
if (i % 5 === 0) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`부적합 사항 ID ${issue.id} 업데이트 실패:`, error);
|
|
||||||
addStatus(`❌ 부적합 사항 ID ${issue.id} 업데이트 실패: ${error.message}`, 'text-red-600');
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 결과 표시
|
|
||||||
if (successCount > 0) {
|
|
||||||
addStatus(`✅ ${successCount}개 부적합 사항 업데이트 완료!`, 'text-green-600 font-bold');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorCount > 0) {
|
|
||||||
addStatus(`❌ ${errorCount}개 부적합 사항 업데이트 실패`, 'text-red-600');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 완료 처리
|
|
||||||
if (errorCount === 0) {
|
|
||||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>모든 업데이트 완료';
|
|
||||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
|
||||||
|
|
||||||
addStatus('🎉 모든 API 데이터 수정이 완료되었습니다!', 'text-green-600 font-bold');
|
|
||||||
addStatus('이제 부적합 조회에서 프로젝트 필터가 정상 작동합니다.', 'text-blue-600');
|
|
||||||
} else {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
|
||||||
btn.className = 'w-full bg-orange-500 text-white py-3 px-4 rounded-lg hover:bg-orange-600 transition-colors font-medium';
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
addStatus(`❌ 오류 발생: ${error.message}`, 'text-red-600');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>프로젝트 ID 수정 - 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">
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50">
|
|
||||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
|
||||||
<i class="fas fa-edit text-blue-500 mr-2"></i>프로젝트 ID 수정
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<h3 class="font-semibold text-blue-800 mb-2">📋 작업 내용</h3>
|
|
||||||
<p class="text-blue-700 text-sm">
|
|
||||||
큰 타임스탬프 ID를 작은 정수 ID로 변경합니다.<br>
|
|
||||||
TKR-25009R 프로젝트 ID: <code>1761264279704</code> → <code>1</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="status" class="space-y-2">
|
|
||||||
<!-- 상태 메시지가 여기에 표시됩니다 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="fixBtn"
|
|
||||||
onclick="fixProjectId()"
|
|
||||||
class="w-full bg-blue-500 text-white py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<i class="fas fa-tools mr-2"></i>프로젝트 ID 수정 시작
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="fix-api-data.html" class="block text-center text-orange-600 hover:text-orange-800 mt-4">
|
|
||||||
<i class="fas fa-wrench mr-1"></i>API 데이터 수정 도구로 이동
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="debug-data.html" class="block text-center text-blue-600 hover:text-blue-800 mt-2">
|
|
||||||
<i class="fas fa-bug mr-1"></i>디버그 도구로 이동
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function addStatus(message, className = 'text-gray-600') {
|
|
||||||
const statusDiv = document.getElementById('status');
|
|
||||||
const p = document.createElement('p');
|
|
||||||
p.className = `text-sm ${className}`;
|
|
||||||
p.innerHTML = `<i class="fas fa-info-circle mr-2"></i>${message}`;
|
|
||||||
statusDiv.appendChild(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fixProjectId() {
|
|
||||||
const btn = document.getElementById('fixBtn');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>수정 중...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 프로젝트 데이터 수정
|
|
||||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
|
||||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
|
||||||
|
|
||||||
if (!mProject) {
|
|
||||||
throw new Error('TKR-25009R 프로젝트를 찾을 수 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldId = mProject.id;
|
|
||||||
const newId = 1;
|
|
||||||
|
|
||||||
addStatus(`기존 프로젝트 ID: ${oldId}`, 'text-blue-600');
|
|
||||||
addStatus(`새로운 프로젝트 ID: ${newId}`, 'text-blue-600');
|
|
||||||
|
|
||||||
// 프로젝트 ID 변경
|
|
||||||
mProject.id = newId;
|
|
||||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
|
||||||
addStatus('✅ 프로젝트 ID 변경 완료', 'text-green-600');
|
|
||||||
|
|
||||||
// 2. 일일 공수 데이터 수정
|
|
||||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
|
||||||
let updatedWorkDays = 0;
|
|
||||||
|
|
||||||
dailyWorkData.forEach(dayData => {
|
|
||||||
if (dayData.projects) {
|
|
||||||
dayData.projects.forEach(project => {
|
|
||||||
if (project.projectId == oldId) {
|
|
||||||
project.projectId = newId;
|
|
||||||
project.projectName = 'TKR-25009R - M Project';
|
|
||||||
updatedWorkDays++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
|
|
||||||
addStatus(`✅ ${updatedWorkDays}개 일일 공수 데이터 업데이트 완료`, 'text-green-600');
|
|
||||||
|
|
||||||
// 3. localStorage 부적합 사항 수정
|
|
||||||
const localIssues = JSON.parse(localStorage.getItem('work-report-issues') || '[]');
|
|
||||||
let updatedIssues = 0;
|
|
||||||
|
|
||||||
localIssues.forEach(issue => {
|
|
||||||
if (issue.projectId == oldId || issue.project_id == oldId) {
|
|
||||||
issue.projectId = newId;
|
|
||||||
issue.project_id = newId;
|
|
||||||
issue.projectName = 'TKR-25009R - M Project';
|
|
||||||
issue.project_name = 'TKR-25009R - M Project';
|
|
||||||
updatedIssues++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem('work-report-issues', JSON.stringify(localIssues));
|
|
||||||
addStatus(`✅ ${updatedIssues}개 localStorage 부적합 사항 업데이트 완료`, 'text-green-600');
|
|
||||||
|
|
||||||
// 완료
|
|
||||||
addStatus('🎉 모든 데이터 ID 수정 완료!', 'text-green-600 font-bold');
|
|
||||||
addStatus('이제 API 데이터 수정 도구를 실행하세요.', 'text-blue-600');
|
|
||||||
|
|
||||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>완료';
|
|
||||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
addStatus(`❌ 오류 발생: ${error.message}`, 'text-red-600');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -214,9 +214,9 @@
|
|||||||
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
||||||
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
||||||
</a>
|
</a>
|
||||||
<a href="admin.html" class="nav-link" style="display:none;" id="adminBtn">
|
<button class="nav-link" style="display:none;" id="adminBtn" onclick="handleAdminClick()">
|
||||||
<i class="fas fa-users-cog mr-2"></i>관리
|
<i class="fas fa-users-cog mr-2"></i>관리
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -502,6 +502,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 관리 버튼 클릭 처리
|
||||||
|
function handleAdminClick() {
|
||||||
|
if (currentUser.role === 'admin') {
|
||||||
|
// 관리자: 사용자 관리 페이지로 이동
|
||||||
|
window.location.href = 'admin.html';
|
||||||
|
} else {
|
||||||
|
// 일반 사용자: 비밀번호 변경 모달 표시
|
||||||
|
showPasswordChangeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 섹션 전환
|
// 섹션 전환
|
||||||
// URL 해시 처리
|
// URL 해시 처리
|
||||||
function handleUrlHash() {
|
function handleUrlHash() {
|
||||||
@@ -593,8 +604,6 @@
|
|||||||
|
|
||||||
// 원본 파일 크기 확인
|
// 원본 파일 크기 확인
|
||||||
const originalSize = file.size;
|
const originalSize = file.size;
|
||||||
console.log(`원본 이미지 크기: ${ImageUtils.formatFileSize(originalSize)}`);
|
|
||||||
|
|
||||||
// 이미지 압축
|
// 이미지 압축
|
||||||
const compressedImage = await ImageUtils.compressImage(file, {
|
const compressedImage = await ImageUtils.compressImage(file, {
|
||||||
maxWidth: 1280,
|
maxWidth: 1280,
|
||||||
@@ -602,11 +611,6 @@
|
|||||||
quality: 0.75
|
quality: 0.75
|
||||||
});
|
});
|
||||||
|
|
||||||
// 압축된 크기 확인
|
|
||||||
const compressedSize = ImageUtils.getBase64Size(compressedImage);
|
|
||||||
console.log(`압축된 이미지 크기: ${ImageUtils.formatFileSize(compressedSize)}`);
|
|
||||||
console.log(`압축률: ${Math.round((1 - compressedSize/originalSize) * 100)}%`);
|
|
||||||
|
|
||||||
currentPhotos.push(compressedImage);
|
currentPhotos.push(compressedImage);
|
||||||
updatePhotoPreview();
|
updatePhotoPreview();
|
||||||
}
|
}
|
||||||
@@ -824,7 +828,6 @@
|
|||||||
updateProgress(90);
|
updateProgress(90);
|
||||||
updateLoadingMessage('완료 처리 중...', '거의 다 되었습니다');
|
updateLoadingMessage('완료 처리 중...', '거의 다 되었습니다');
|
||||||
|
|
||||||
console.log(`업로드 완료: ${uploadTime}ms`);
|
|
||||||
|
|
||||||
updateProgress(100);
|
updateProgress(100);
|
||||||
|
|
||||||
@@ -1453,6 +1456,125 @@
|
|||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 비밀번호 변경 모달 표시
|
||||||
|
function showPasswordChangeModal() {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
||||||
|
modal.onclick = (e) => {
|
||||||
|
if (e.target === modal) modal.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-semibold">비밀번호 변경</h3>
|
||||||
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-600">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="passwordChangeForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">현재 비밀번호</label>
|
||||||
|
<input type="password" id="currentPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
|
||||||
|
<input type="password" id="newPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required minlength="6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label>
|
||||||
|
<input type="password" id="confirmPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 pt-4">
|
||||||
|
<button type="button" onclick="this.closest('.fixed').remove()"
|
||||||
|
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
||||||
|
변경
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// 폼 제출 이벤트 처리
|
||||||
|
document.getElementById('passwordChangeForm').addEventListener('submit', handlePasswordChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비밀번호 변경 처리
|
||||||
|
async function handlePasswordChange(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const currentPassword = document.getElementById('currentPassword').value;
|
||||||
|
const newPassword = document.getElementById('newPassword').value;
|
||||||
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
|
||||||
|
// 새 비밀번호 확인
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
alert('새 비밀번호가 일치하지 않습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 비밀번호 확인 (localStorage 기반)
|
||||||
|
let users = JSON.parse(localStorage.getItem('work-report-users') || '[]');
|
||||||
|
|
||||||
|
// 기본 사용자가 없으면 생성
|
||||||
|
if (users.length === 0) {
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
username: 'hyungi',
|
||||||
|
full_name: '관리자',
|
||||||
|
password: 'djg3-jj34-X3Q3',
|
||||||
|
role: 'admin'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = users.find(u => u.username === currentUser.username);
|
||||||
|
|
||||||
|
// 사용자가 없으면 기본값으로 생성
|
||||||
|
if (!user) {
|
||||||
|
const username = currentUser.username;
|
||||||
|
user = {
|
||||||
|
username: username,
|
||||||
|
full_name: username === 'hyungi' ? '관리자' : username,
|
||||||
|
password: 'djg3-jj34-X3Q3',
|
||||||
|
role: username === 'hyungi' ? 'admin' : 'user'
|
||||||
|
};
|
||||||
|
users.push(user);
|
||||||
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.password !== currentPassword) {
|
||||||
|
alert('현재 비밀번호가 올바르지 않습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 비밀번호 변경
|
||||||
|
user.password = newPassword;
|
||||||
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
||||||
|
|
||||||
|
// 현재 사용자 정보도 업데이트
|
||||||
|
currentUser.password = newPassword;
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
||||||
|
|
||||||
|
showToastMessage('비밀번호가 성공적으로 변경되었습니다.');
|
||||||
|
document.querySelector('.fixed').remove(); // 모달 닫기
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert('비밀번호 변경에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 토스트 메시지 표시
|
// 토스트 메시지 표시
|
||||||
function showToastMessage(message, type = 'success') {
|
function showToastMessage(message, type = 'success') {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
@@ -1517,7 +1639,6 @@
|
|||||||
|
|
||||||
// localStorage의 프로젝트별 데이터 우선 사용 (프로젝트별 분리 지원)
|
// localStorage의 프로젝트별 데이터 우선 사용 (프로젝트별 분리 지원)
|
||||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
||||||
|
|
||||||
if (selectedProjectId) {
|
if (selectedProjectId) {
|
||||||
// 선택된 프로젝트의 일일 공수만 합계
|
// 선택된 프로젝트의 일일 공수만 합계
|
||||||
dailyWorkData.forEach(dayWork => {
|
dailyWorkData.forEach(dayWork => {
|
||||||
|
|||||||
@@ -106,9 +106,9 @@
|
|||||||
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
<a href="project-management.html" class="nav-link" style="display:none;" id="projectBtn">
|
||||||
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
<i class="fas fa-folder-open mr-2"></i>프로젝트 관리
|
||||||
</a>
|
</a>
|
||||||
<a href="admin.html" class="nav-link" style="display:none;" id="adminBtn">
|
<button class="nav-link" style="display:none;" id="adminBtn" onclick="handleAdminClick()">
|
||||||
<i class="fas fa-users-cog mr-2"></i>사용자 관리
|
<i class="fas fa-users-cog mr-2"></i>사용자 관리
|
||||||
</a>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -317,8 +317,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function filterIssues() {
|
function filterIssues() {
|
||||||
console.log('필터링 시작 - 전체 이슈:', issues.length);
|
|
||||||
|
|
||||||
// 필터 값 가져오기
|
// 필터 값 가져오기
|
||||||
const selectedProjectId = document.getElementById('projectFilter').value;
|
const selectedProjectId = document.getElementById('projectFilter').value;
|
||||||
const reviewStatusFilter = document.getElementById('reviewStatusFilter').value;
|
const reviewStatusFilter = document.getElementById('reviewStatusFilter').value;
|
||||||
@@ -332,7 +330,6 @@
|
|||||||
const issueProjectId = issue.project_id || issue.projectId;
|
const issueProjectId = issue.project_id || issue.projectId;
|
||||||
return issueProjectId && (issueProjectId == selectedProjectId || issueProjectId.toString() === selectedProjectId.toString());
|
return issueProjectId && (issueProjectId == selectedProjectId || issueProjectId.toString() === selectedProjectId.toString());
|
||||||
});
|
});
|
||||||
console.log('프로젝트 필터 후:', filteredIssues.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 검토 상태 필터 적용
|
// 검토 상태 필터 적용
|
||||||
@@ -341,13 +338,11 @@
|
|||||||
const isCompleted = isReviewCompleted(issue);
|
const isCompleted = isReviewCompleted(issue);
|
||||||
return reviewStatusFilter === 'completed' ? isCompleted : !isCompleted;
|
return reviewStatusFilter === 'completed' ? isCompleted : !isCompleted;
|
||||||
});
|
});
|
||||||
console.log('검토 상태 필터 후:', filteredIssues.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 날짜 필터 적용
|
// 날짜 필터 적용
|
||||||
if (dateFilter) {
|
if (dateFilter) {
|
||||||
filteredIssues = filterByDate(filteredIssues, dateFilter);
|
filteredIssues = filterByDate(filteredIssues, dateFilter);
|
||||||
console.log('날짜 필터 후:', filteredIssues.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전역 변수에 필터링된 결과 저장
|
// 전역 변수에 필터링된 결과 저장
|
||||||
@@ -599,6 +594,136 @@
|
|||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 관리 버튼 클릭 처리
|
||||||
|
function handleAdminClick() {
|
||||||
|
if (currentUser.role === 'admin') {
|
||||||
|
// 관리자: 사용자 관리 페이지로 이동
|
||||||
|
window.location.href = 'admin.html';
|
||||||
|
} else {
|
||||||
|
// 일반 사용자: 비밀번호 변경 모달 표시
|
||||||
|
showPasswordChangeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비밀번호 변경 모달 표시
|
||||||
|
function showPasswordChangeModal() {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
||||||
|
modal.onclick = (e) => {
|
||||||
|
if (e.target === modal) modal.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 class="text-lg font-semibold">비밀번호 변경</h3>
|
||||||
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-600">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="passwordChangeForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">현재 비밀번호</label>
|
||||||
|
<input type="password" id="currentPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
|
||||||
|
<input type="password" id="newPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required minlength="6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label>
|
||||||
|
<input type="password" id="confirmPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 pt-4">
|
||||||
|
<button type="button" onclick="this.closest('.fixed').remove()"
|
||||||
|
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
||||||
|
변경
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// 폼 제출 이벤트 처리
|
||||||
|
document.getElementById('passwordChangeForm').addEventListener('submit', handlePasswordChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 비밀번호 변경 처리
|
||||||
|
async function handlePasswordChange(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const currentPassword = document.getElementById('currentPassword').value;
|
||||||
|
const newPassword = document.getElementById('newPassword').value;
|
||||||
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||||
|
|
||||||
|
// 새 비밀번호 확인
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
alert('새 비밀번호가 일치하지 않습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 비밀번호 확인 (localStorage 기반)
|
||||||
|
let users = JSON.parse(localStorage.getItem('work-report-users') || '[]');
|
||||||
|
|
||||||
|
// 기본 사용자가 없으면 생성
|
||||||
|
if (users.length === 0) {
|
||||||
|
users = [
|
||||||
|
{
|
||||||
|
username: 'hyungi',
|
||||||
|
full_name: '관리자',
|
||||||
|
password: 'djg3-jj34-X3Q3',
|
||||||
|
role: 'admin'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = users.find(u => u.username === currentUser.username);
|
||||||
|
|
||||||
|
// 사용자가 없으면 기본값으로 생성
|
||||||
|
if (!user) {
|
||||||
|
const username = currentUser.username;
|
||||||
|
user = {
|
||||||
|
username: username,
|
||||||
|
full_name: username === 'hyungi' ? '관리자' : username,
|
||||||
|
password: 'djg3-jj34-X3Q3',
|
||||||
|
role: username === 'hyungi' ? 'admin' : 'user'
|
||||||
|
};
|
||||||
|
users.push(user);
|
||||||
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.password !== currentPassword) {
|
||||||
|
alert('현재 비밀번호가 올바르지 않습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 비밀번호 변경
|
||||||
|
user.password = newPassword;
|
||||||
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
||||||
|
|
||||||
|
// 현재 사용자 정보도 업데이트
|
||||||
|
currentUser.password = newPassword;
|
||||||
|
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
||||||
|
|
||||||
|
alert('비밀번호가 성공적으로 변경되었습니다.');
|
||||||
|
document.querySelector('.fixed').remove(); // 모달 닫기
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert('비밀번호 변경에 실패했습니다: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 로그아웃 함수
|
// 로그아웃 함수
|
||||||
function logout() {
|
function logout() {
|
||||||
localStorage.removeItem('currentUser');
|
localStorage.removeItem('currentUser');
|
||||||
|
|||||||
@@ -1,307 +0,0 @@
|
|||||||
<!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="/static/js/api.js?v=20250917"></script>
|
|
||||||
</head>
|
|
||||||
<body class="bg-gray-50">
|
|
||||||
<div class="container mx-auto px-4 py-8 max-w-2xl">
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">
|
|
||||||
<i class="fas fa-database text-blue-500 mr-2"></i>데이터 마이그레이션
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
||||||
<h3 class="font-semibold text-yellow-800 mb-2">⚠️ 주의사항</h3>
|
|
||||||
<p class="text-yellow-700 text-sm">
|
|
||||||
이 작업은 기존 데이터를 "M Project"로 마이그레이션합니다.<br>
|
|
||||||
관리자(hyungi) 계정으로 로그인한 상태에서만 실행하세요.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="status" class="space-y-2">
|
|
||||||
<!-- 상태 메시지가 여기에 표시됩니다 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="migrateBtn"
|
|
||||||
onclick="startMigration()"
|
|
||||||
class="w-full bg-blue-500 text-white py-3 px-4 rounded-lg hover:bg-blue-600 transition-colors font-medium"
|
|
||||||
>
|
|
||||||
<i class="fas fa-play mr-2"></i>마이그레이션 시작
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a href="index.html" class="block text-center text-gray-600 hover:text-gray-800 mt-4">
|
|
||||||
<i class="fas fa-arrow-left mr-1"></i>메인으로 돌아가기
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let currentUser = null;
|
|
||||||
|
|
||||||
// 페이지 로드 시 사용자 확인
|
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const userData = localStorage.getItem('currentUser');
|
|
||||||
if (!userData) {
|
|
||||||
alert('로그인이 필요합니다.');
|
|
||||||
window.location.href = 'index.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
currentUser = JSON.parse(userData);
|
|
||||||
} catch (e) {
|
|
||||||
currentUser = { username: userData };
|
|
||||||
}
|
|
||||||
|
|
||||||
const username = currentUser.username || currentUser;
|
|
||||||
if (username !== 'hyungi' && currentUser.role !== 'admin') {
|
|
||||||
alert('관리자만 접근 가능합니다.');
|
|
||||||
window.location.href = 'index.html';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
addStatus('✅ 관리자 권한 확인됨', 'text-green-600');
|
|
||||||
});
|
|
||||||
|
|
||||||
function addStatus(message, className = 'text-gray-600') {
|
|
||||||
const statusDiv = document.getElementById('status');
|
|
||||||
const p = document.createElement('p');
|
|
||||||
p.className = `text-sm ${className}`;
|
|
||||||
p.innerHTML = `<i class="fas fa-info-circle mr-2"></i>${message}`;
|
|
||||||
statusDiv.appendChild(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startMigration() {
|
|
||||||
const btn = document.getElementById('migrateBtn');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>마이그레이션 중...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. M Project 생성
|
|
||||||
await createMProject();
|
|
||||||
|
|
||||||
// 2. 기존 부적합 사항 마이그레이션
|
|
||||||
await migrateIssues();
|
|
||||||
|
|
||||||
// 3. 기존 368시간 데이터 생성
|
|
||||||
await createSampleHours();
|
|
||||||
|
|
||||||
addStatus('🎉 모든 마이그레이션이 완료되었습니다!', 'text-green-600 font-bold');
|
|
||||||
|
|
||||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>완료';
|
|
||||||
btn.className = 'w-full bg-green-500 text-white py-3 px-4 rounded-lg font-medium';
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
addStatus(`❌ 오류 발생: ${error.message}`, 'text-red-600');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '<i class="fas fa-redo mr-2"></i>다시 시도';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createMProject() {
|
|
||||||
addStatus('기존 M Project 확인 중...');
|
|
||||||
|
|
||||||
// 기존 프로젝트 확인
|
|
||||||
let projects = [];
|
|
||||||
const saved = localStorage.getItem('work-report-projects');
|
|
||||||
if (saved) {
|
|
||||||
projects = JSON.parse(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 TKR-25009R M Project 찾기
|
|
||||||
let existingMProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
|
||||||
if (existingMProject) {
|
|
||||||
addStatus('✅ 기존 TKR-25009R M Project 발견', 'text-green-600');
|
|
||||||
return existingMProject.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// M-2024-001로 생성된 잘못된 프로젝트 찾기
|
|
||||||
const wrongProject = projects.find(p => p.jobNo === 'M-2024-001');
|
|
||||||
if (wrongProject) {
|
|
||||||
// 잘못된 프로젝트를 올바른 Job No.로 수정
|
|
||||||
wrongProject.jobNo = 'TKR-25009R';
|
|
||||||
wrongProject.projectName = 'M Project';
|
|
||||||
addStatus('✅ 기존 프로젝트를 TKR-25009R로 수정 완료', 'text-green-600');
|
|
||||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
|
||||||
return wrongProject.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 새로 생성 (기존 프로젝트가 없는 경우)
|
|
||||||
const mProject = {
|
|
||||||
id: Date.now(),
|
|
||||||
jobNo: 'TKR-25009R',
|
|
||||||
projectName: 'M Project',
|
|
||||||
createdBy: 'hyungi',
|
|
||||||
createdByName: '관리자',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
isActive: true
|
|
||||||
};
|
|
||||||
|
|
||||||
projects.push(mProject);
|
|
||||||
localStorage.setItem('work-report-projects', JSON.stringify(projects));
|
|
||||||
|
|
||||||
addStatus('✅ TKR-25009R M Project 생성 완료', 'text-green-600');
|
|
||||||
return mProject.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateIssues() {
|
|
||||||
addStatus('기존 부적합 사항 마이그레이션 중...');
|
|
||||||
|
|
||||||
// 기존 부적합 사항 로드 (localStorage와 API 모두 확인)
|
|
||||||
let issues = [];
|
|
||||||
|
|
||||||
// 먼저 localStorage에서 확인
|
|
||||||
const savedLocal = localStorage.getItem('work-report-issues');
|
|
||||||
if (savedLocal) {
|
|
||||||
issues = JSON.parse(savedLocal);
|
|
||||||
addStatus('localStorage에서 부적합 사항 발견', 'text-blue-600');
|
|
||||||
}
|
|
||||||
|
|
||||||
// API에서도 확인 (현재 시스템이 API 기반이므로)
|
|
||||||
try {
|
|
||||||
if (typeof IssuesAPI !== 'undefined') {
|
|
||||||
const apiIssues = await IssuesAPI.getAll();
|
|
||||||
if (apiIssues && apiIssues.length > 0) {
|
|
||||||
issues = apiIssues;
|
|
||||||
addStatus('API에서 부적합 사항 발견', 'text-blue-600');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
addStatus('API 조회 실패, localStorage 데이터 사용', 'text-yellow-600');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (issues.length === 0) {
|
|
||||||
addStatus('마이그레이션할 부적합 사항이 없습니다.', 'text-yellow-600');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TKR-25009R M Project ID 가져오기
|
|
||||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
|
||||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
|
||||||
|
|
||||||
if (!mProject) {
|
|
||||||
throw new Error('TKR-25009R M Project를 찾을 수 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모든 부적합 사항에 프로젝트 ID 추가
|
|
||||||
let migratedCount = 0;
|
|
||||||
for (let issue of issues) {
|
|
||||||
if (!issue.project_id && !issue.projectId) {
|
|
||||||
// API 방식과 localStorage 방식 모두 지원
|
|
||||||
issue.project_id = mProject.id;
|
|
||||||
issue.projectId = mProject.id;
|
|
||||||
issue.project_name = 'TKR-25009R - M Project';
|
|
||||||
issue.projectName = 'TKR-25009R - M Project';
|
|
||||||
migratedCount++;
|
|
||||||
|
|
||||||
// API로 업데이트 시도
|
|
||||||
try {
|
|
||||||
if (typeof IssuesAPI !== 'undefined' && issue.id) {
|
|
||||||
await IssuesAPI.update(issue.id, {
|
|
||||||
project_id: mProject.id,
|
|
||||||
project_name: 'TKR-25009R - M Project'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('API 업데이트 실패, localStorage만 업데이트');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// localStorage에도 저장
|
|
||||||
localStorage.setItem('work-report-issues', JSON.stringify(issues));
|
|
||||||
|
|
||||||
addStatus(`✅ ${migratedCount}개 부적합 사항 마이그레이션 완료`, 'text-green-600');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSampleHours() {
|
|
||||||
addStatus('368시간 샘플 데이터 생성 중...');
|
|
||||||
|
|
||||||
// TKR-25009R M Project ID 가져오기
|
|
||||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
|
||||||
const mProject = projects.find(p => p.jobNo === 'TKR-25009R');
|
|
||||||
|
|
||||||
if (!mProject) {
|
|
||||||
throw new Error('TKR-25009R M Project를 찾을 수 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 일일 공수 데이터 로드
|
|
||||||
let dailyWorkData = [];
|
|
||||||
const saved = localStorage.getItem('daily-work-data');
|
|
||||||
if (saved) {
|
|
||||||
dailyWorkData = JSON.parse(saved);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기존 데이터 중 잘못된 프로젝트로 등록된 것들 수정
|
|
||||||
let updatedCount = 0;
|
|
||||||
dailyWorkData.forEach(dayData => {
|
|
||||||
if (dayData.projects) {
|
|
||||||
dayData.projects.forEach(project => {
|
|
||||||
if (project.projectName && project.projectName.includes('M-2024-001')) {
|
|
||||||
project.projectId = mProject.id;
|
|
||||||
project.projectName = 'TKR-25009R - M Project';
|
|
||||||
updatedCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updatedCount > 0) {
|
|
||||||
addStatus(`✅ 기존 ${updatedCount}개 프로젝트 데이터를 TKR-25009R로 수정`, 'text-green-600');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 368시간을 여러 날짜에 분산해서 생성 (기존 데이터가 없는 경우만)
|
|
||||||
const workDays = [
|
|
||||||
{ date: '2024-10-01', hours: 48 },
|
|
||||||
{ date: '2024-10-02', hours: 52 },
|
|
||||||
{ date: '2024-10-03', hours: 44 },
|
|
||||||
{ date: '2024-10-04', hours: 40 },
|
|
||||||
{ date: '2024-10-07', hours: 56 },
|
|
||||||
{ date: '2024-10-08', hours: 48 },
|
|
||||||
{ date: '2024-10-09', hours: 36 },
|
|
||||||
{ date: '2024-10-10', hours: 44 }
|
|
||||||
];
|
|
||||||
|
|
||||||
let addedDays = 0;
|
|
||||||
workDays.forEach(workDay => {
|
|
||||||
// 해당 날짜에 이미 데이터가 있는지 확인
|
|
||||||
const existingData = dailyWorkData.find(d => d.date === workDay.date);
|
|
||||||
if (!existingData) {
|
|
||||||
const newData = {
|
|
||||||
date: workDay.date,
|
|
||||||
projects: [{
|
|
||||||
projectId: mProject.id,
|
|
||||||
projectName: 'TKR-25009R - M Project',
|
|
||||||
hours: workDay.hours
|
|
||||||
}],
|
|
||||||
totalHours: workDay.hours,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
createdBy: 'hyungi'
|
|
||||||
};
|
|
||||||
|
|
||||||
dailyWorkData.push(newData);
|
|
||||||
addedDays++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 저장
|
|
||||||
localStorage.setItem('daily-work-data', JSON.stringify(dailyWorkData));
|
|
||||||
|
|
||||||
const totalCreatedHours = workDays.reduce((sum, day) => sum + day.hours, 0);
|
|
||||||
if (addedDays > 0) {
|
|
||||||
addStatus(`✅ ${addedDays}일간 총 ${totalCreatedHours}시간 데이터 생성 완료`, 'text-green-600');
|
|
||||||
} else {
|
|
||||||
addStatus('✅ 기존 시간 데이터 수정 완료', 'text-green-600');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -125,6 +125,13 @@ const AuthAPI = {
|
|||||||
current_password: currentPassword,
|
current_password: currentPassword,
|
||||||
new_password: newPassword
|
new_password: newPassword
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
resetPassword: (userId, newPassword = '000000') => apiRequest(`/auth/users/${userId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({
|
||||||
|
password: newPassword
|
||||||
|
})
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
671
index.html
671
index.html
@@ -1,671 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ko">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>작업보고서 시스템</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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field {
|
|
||||||
border: 1px solid var(--gray-300);
|
|
||||||
background: white;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-field:focus {
|
|
||||||
border-color: var(--primary);
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-new {
|
|
||||||
background-color: #dbeafe;
|
|
||||||
color: #1e40af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-progress {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-complete {
|
|
||||||
background-color: #d1fae5;
|
|
||||||
color: #065f46;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
color: #6b7280;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
background-color: var(--gray-100);
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- 로그인 화면 -->
|
|
||||||
<div id="loginScreen" class="min-h-screen flex items-center justify-center p-4 bg-gray-50">
|
|
||||||
<div class="bg-white rounded-xl shadow-lg p-8 w-full max-w-sm">
|
|
||||||
<div class="text-center mb-6">
|
|
||||||
<i class="fas fa-clipboard-check text-5xl text-blue-500 mb-4"></i>
|
|
||||||
<h1 class="text-2xl font-bold text-gray-800">작업보고서 시스템</h1>
|
|
||||||
<p class="text-gray-600 text-sm mt-2">부적합 사항 관리 및 공수 계산</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="loginForm" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">아이디</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="userId"
|
|
||||||
class="input-field w-full px-4 py-2 rounded-lg"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">비밀번호</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
class="input-field w-full px-4 py-2 rounded-lg"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-primary w-full py-2 rounded-lg font-medium">
|
|
||||||
로그인
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 메인 화면 -->
|
|
||||||
<div id="mainScreen" class="hidden min-h-screen bg-gray-50">
|
|
||||||
<!-- 헤더 -->
|
|
||||||
<header class="bg-white shadow-sm sticky top-0 z-50">
|
|
||||||
<div class="container mx-auto px-4 py-3">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<h1 class="text-xl font-bold text-gray-800">
|
|
||||||
<i class="fas fa-clipboard-check text-blue-500 mr-2"></i>작업보고서
|
|
||||||
</h1>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<span class="text-sm text-gray-600" id="userDisplay"></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>
|
|
||||||
|
|
||||||
<!-- 네비게이션 -->
|
|
||||||
<nav class="bg-white border-b">
|
|
||||||
<div class="container mx-auto px-4">
|
|
||||||
<div class="flex gap-2 py-2 overflow-x-auto">
|
|
||||||
<a href="daily-work.html" class="nav-link">
|
|
||||||
<i class="fas fa-calendar-check mr-2"></i>일일 공수
|
|
||||||
</a>
|
|
||||||
<button class="nav-link active" onclick="showSection('report')">
|
|
||||||
<i class="fas fa-camera-retro mr-2"></i>부적합 등록
|
|
||||||
</button>
|
|
||||||
<button class="nav-link" onclick="showSection('list')">
|
|
||||||
<i class="fas fa-list mr-2"></i>목록 관리
|
|
||||||
</button>
|
|
||||||
<button class="nav-link" onclick="showSection('summary')">
|
|
||||||
<i class="fas fa-chart-bar mr-2"></i>보고서
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- 부적합 등록 섹션 (모바일 최적화) -->
|
|
||||||
<section id="reportSection" class="container mx-auto px-4 py-6 max-w-lg">
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">
|
|
||||||
<i class="fas fa-exclamation-triangle text-yellow-500 mr-2"></i>부적합 사항 등록
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<form id="reportForm" class="space-y-4">
|
|
||||||
<!-- 사진 업로드 -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">사진</label>
|
|
||||||
<div
|
|
||||||
id="photoUpload"
|
|
||||||
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-blue-400 transition-colors"
|
|
||||||
onclick="document.getElementById('photoInput').click()"
|
|
||||||
>
|
|
||||||
<div id="photoPreview" class="hidden mb-4">
|
|
||||||
<img id="previewImg" class="max-h-48 mx-auto rounded-lg">
|
|
||||||
</div>
|
|
||||||
<div id="photoPlaceholder">
|
|
||||||
<i class="fas fa-camera text-4xl text-gray-400 mb-2"></i>
|
|
||||||
<p class="text-gray-600">사진 촬영 또는 선택</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="file" id="photoInput" accept="image/*" capture="camera" class="hidden">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 카테고리 -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">카테고리</label>
|
|
||||||
<select id="category" class="input-field w-full px-4 py-2 rounded-lg" required>
|
|
||||||
<option value="">선택하세요</option>
|
|
||||||
<option value="material_missing">자재누락</option>
|
|
||||||
<option value="dimension_defect">치수불량</option>
|
|
||||||
<option value="incoming_defect">입고자재 불량</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 간단 설명 -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">간단 설명</label>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
rows="3"
|
|
||||||
class="input-field w-full px-4 py-2 rounded-lg resize-none"
|
|
||||||
placeholder="발견된 문제를 간단히 설명하세요"
|
|
||||||
required
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn-primary w-full py-3 rounded-lg font-medium text-lg">
|
|
||||||
<i class="fas fa-check mr-2"></i>등록하기
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 목록 관리 섹션 -->
|
|
||||||
<section id="listSection" class="hidden container mx-auto px-4 py-6">
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">부적합 사항 목록</h2>
|
|
||||||
<div id="issueList" class="space-y-3">
|
|
||||||
<!-- 목록이 여기에 표시됩니다 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 상세보기 모달 -->
|
|
||||||
<div id="detailModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4" onclick="if(event.target === this) closeDetailModal()">
|
|
||||||
<div class="bg-white rounded-xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto" onclick="event.stopPropagation()">
|
|
||||||
<div class="sticky top-0 bg-white border-b p-4 flex justify-between items-center">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-800">부적합 사항 상세</h3>
|
|
||||||
<button onclick="closeDetailModal()" class="text-gray-500 hover:text-gray-700">
|
|
||||||
<i class="fas fa-times text-xl"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6">
|
|
||||||
<!-- 사진 -->
|
|
||||||
<div id="modalPhoto" class="mb-6"></div>
|
|
||||||
|
|
||||||
<!-- 수정 가능한 정보 -->
|
|
||||||
<div class="mb-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">카테고리</label>
|
|
||||||
<select id="modalCategory" class="input-field w-full px-4 py-2 rounded-lg">
|
|
||||||
<option value="material_missing">자재누락</option>
|
|
||||||
<option value="dimension_defect">치수불량</option>
|
|
||||||
<option value="incoming_defect">입고자재 불량</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">설명</label>
|
|
||||||
<textarea
|
|
||||||
id="modalDescription"
|
|
||||||
rows="3"
|
|
||||||
class="input-field w-full px-4 py-2 rounded-lg resize-none"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-4 text-sm text-gray-500 pt-2">
|
|
||||||
<span><i class="fas fa-user mr-1"></i><span id="modalReporter"></span></span>
|
|
||||||
<span><i class="fas fa-calendar mr-1"></i><span id="modalDate"></span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 작업 시간 입력 -->
|
|
||||||
<div class="border-t pt-6">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">해결하는데 걸린 총 시간</label>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="workHours"
|
|
||||||
step="0.5"
|
|
||||||
min="0"
|
|
||||||
class="input-field flex-1 px-4 py-2 rounded-lg"
|
|
||||||
placeholder="예: 2.5"
|
|
||||||
>
|
|
||||||
<span class="text-gray-600">시간</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onclick="saveIssueDetails()" class="btn-primary w-full py-2 rounded-lg font-medium">
|
|
||||||
<i class="fas fa-save mr-2"></i>저장하기
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 보고서 섹션 -->
|
|
||||||
<section id="summarySection" class="hidden container mx-auto px-4 py-6">
|
|
||||||
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
||||||
<div class="flex justify-between items-center mb-6">
|
|
||||||
<h2 class="text-lg font-semibold text-gray-800">작업 보고서</h2>
|
|
||||||
<button onclick="printReport()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
|
||||||
<i class="fas fa-print mr-2"></i>인쇄
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="reportContent">
|
|
||||||
<!-- 보고서 내용이 여기에 표시됩니다 -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 사용자 데이터
|
|
||||||
const users = {
|
|
||||||
'inspector1': { password: 'pass123', name: '검사자1' },
|
|
||||||
'inspector2': { password: 'pass456', name: '검사자2' },
|
|
||||||
'admin': { password: 'admin123', name: '관리자' }
|
|
||||||
};
|
|
||||||
|
|
||||||
let currentUser = null;
|
|
||||||
let currentPhoto = null;
|
|
||||||
let issues = [];
|
|
||||||
|
|
||||||
// 로그인
|
|
||||||
document.getElementById('loginForm').addEventListener('submit', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const userId = document.getElementById('userId').value;
|
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
|
|
||||||
if (users[userId] && users[userId].password === password) {
|
|
||||||
currentUser = { id: userId, ...users[userId] };
|
|
||||||
document.getElementById('userDisplay').textContent = currentUser.name;
|
|
||||||
document.getElementById('loginScreen').classList.add('hidden');
|
|
||||||
document.getElementById('mainScreen').classList.remove('hidden');
|
|
||||||
loadIssues();
|
|
||||||
} else {
|
|
||||||
alert('아이디 또는 비밀번호가 올바르지 않습니다.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 로그아웃
|
|
||||||
function logout() {
|
|
||||||
currentUser = null;
|
|
||||||
document.getElementById('loginScreen').classList.remove('hidden');
|
|
||||||
document.getElementById('mainScreen').classList.add('hidden');
|
|
||||||
document.getElementById('loginForm').reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 섹션 전환
|
|
||||||
function showSection(section) {
|
|
||||||
// 모든 섹션 숨기기
|
|
||||||
document.querySelectorAll('section').forEach(s => s.classList.add('hidden'));
|
|
||||||
// 선택된 섹션 표시
|
|
||||||
document.getElementById(section + 'Section').classList.remove('hidden');
|
|
||||||
|
|
||||||
// 네비게이션 활성화 상태 변경
|
|
||||||
document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
|
|
||||||
event.target.closest('.nav-link').classList.add('active');
|
|
||||||
|
|
||||||
// 섹션별 초기화
|
|
||||||
if (section === 'list') {
|
|
||||||
displayIssueList();
|
|
||||||
} else if (section === 'summary') {
|
|
||||||
generateReport();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 사진 업로드
|
|
||||||
document.getElementById('photoInput').addEventListener('change', (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
currentPhoto = e.target.result;
|
|
||||||
document.getElementById('previewImg').src = currentPhoto;
|
|
||||||
document.getElementById('photoPreview').classList.remove('hidden');
|
|
||||||
document.getElementById('photoPlaceholder').classList.add('hidden');
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 부적합 사항 등록
|
|
||||||
document.getElementById('reportForm').addEventListener('submit', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const issue = {
|
|
||||||
id: Date.now(),
|
|
||||||
photo: currentPhoto,
|
|
||||||
category: document.getElementById('category').value,
|
|
||||||
description: document.getElementById('description').value,
|
|
||||||
status: 'new',
|
|
||||||
reporter: currentUser.id,
|
|
||||||
reporterName: currentUser.name,
|
|
||||||
reportDate: new Date().toISOString(),
|
|
||||||
workHours: 0,
|
|
||||||
detailNotes: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
issues.push(issue);
|
|
||||||
saveIssues();
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
alert('부적합 사항이 등록되었습니다.');
|
|
||||||
|
|
||||||
// 폼 초기화
|
|
||||||
document.getElementById('reportForm').reset();
|
|
||||||
currentPhoto = null;
|
|
||||||
document.getElementById('photoPreview').classList.add('hidden');
|
|
||||||
document.getElementById('photoPlaceholder').classList.remove('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 데이터 저장/로드
|
|
||||||
function saveIssues() {
|
|
||||||
localStorage.setItem('work-report-issues', JSON.stringify(issues));
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadIssues() {
|
|
||||||
const saved = localStorage.getItem('work-report-issues');
|
|
||||||
if (saved) {
|
|
||||||
issues = JSON.parse(saved);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 목록 표시
|
|
||||||
function displayIssueList() {
|
|
||||||
const container = document.getElementById('issueList');
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
if (issues.length === 0) {
|
|
||||||
container.innerHTML = '<p class="text-gray-500 text-center py-8">등록된 부적합 사항이 없습니다.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
issues.forEach(issue => {
|
|
||||||
const categoryNames = {
|
|
||||||
material_missing: '자재누락',
|
|
||||||
dimension_defect: '치수불량',
|
|
||||||
incoming_defect: '입고자재 불량'
|
|
||||||
};
|
|
||||||
|
|
||||||
const statusBadges = {
|
|
||||||
new: 'status-new',
|
|
||||||
progress: 'status-progress',
|
|
||||||
complete: 'status-complete'
|
|
||||||
};
|
|
||||||
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer';
|
|
||||||
div.innerHTML = `
|
|
||||||
<div class="flex items-start gap-4">
|
|
||||||
${issue.photo ? `<img src="${issue.photo}" class="w-20 h-20 object-cover rounded-lg">` : ''}
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex justify-between items-start mb-2">
|
|
||||||
<h3 class="font-medium text-gray-800">${categoryNames[issue.category] || issue.category}</h3>
|
|
||||||
<span class="status-badge ${statusBadges[issue.status]}">${
|
|
||||||
issue.status === 'new' ? '신규' :
|
|
||||||
issue.status === 'progress' ? '진행중' : '완료'
|
|
||||||
}</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-gray-600 mb-2">${issue.description}</p>
|
|
||||||
<div class="flex items-center gap-4 text-sm">
|
|
||||||
<span class="text-gray-500">
|
|
||||||
<i class="fas fa-calendar mr-1"></i>
|
|
||||||
${new Date(issue.reportDate).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-500">
|
|
||||||
<i class="fas fa-clock mr-1"></i>
|
|
||||||
${issue.workHours}시간
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
div.onclick = () => showIssueDetail(issue.id);
|
|
||||||
container.appendChild(div);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 현재 편집 중인 이슈 ID
|
|
||||||
let currentEditingIssueId = null;
|
|
||||||
|
|
||||||
// 부적합 사항 상세보기
|
|
||||||
function showIssueDetail(issueId) {
|
|
||||||
const issue = issues.find(i => i.id === issueId);
|
|
||||||
if (!issue) return;
|
|
||||||
|
|
||||||
currentEditingIssueId = issueId;
|
|
||||||
|
|
||||||
const categoryNames = {
|
|
||||||
material_missing: '자재누락',
|
|
||||||
dimension_defect: '치수불량',
|
|
||||||
incoming_defect: '입고자재 불량'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 사진 표시
|
|
||||||
const photoContainer = document.getElementById('modalPhoto');
|
|
||||||
if (issue.photo) {
|
|
||||||
photoContainer.innerHTML = `<img src="${issue.photo}" class="w-full rounded-lg shadow-md">`;
|
|
||||||
} else {
|
|
||||||
photoContainer.innerHTML = '<div class="bg-gray-100 rounded-lg p-8 text-center text-gray-500">사진 없음</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 정보 표시
|
|
||||||
document.getElementById('modalCategory').value = issue.category;
|
|
||||||
document.getElementById('modalDescription').value = issue.description;
|
|
||||||
document.getElementById('modalReporter').textContent = issue.reporterName;
|
|
||||||
document.getElementById('modalDate').textContent = new Date(issue.reportDate).toLocaleDateString();
|
|
||||||
|
|
||||||
// 작업 시간
|
|
||||||
document.getElementById('workHours').value = issue.workHours || '';
|
|
||||||
|
|
||||||
// 모달 표시
|
|
||||||
document.getElementById('detailModal').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모달 닫기
|
|
||||||
function closeDetailModal() {
|
|
||||||
document.getElementById('detailModal').classList.add('hidden');
|
|
||||||
currentEditingIssueId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상세 정보 저장
|
|
||||||
function saveIssueDetails() {
|
|
||||||
if (!currentEditingIssueId) return;
|
|
||||||
|
|
||||||
const issue = issues.find(i => i.id === currentEditingIssueId);
|
|
||||||
if (!issue) return;
|
|
||||||
|
|
||||||
// 값 가져오기
|
|
||||||
const category = document.getElementById('modalCategory').value;
|
|
||||||
const description = document.getElementById('modalDescription').value.trim();
|
|
||||||
const workHours = parseFloat(document.getElementById('workHours').value) || 0;
|
|
||||||
|
|
||||||
// 유효성 검사
|
|
||||||
if (!description) {
|
|
||||||
alert('설명을 입력해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 업데이트
|
|
||||||
issue.category = category;
|
|
||||||
issue.description = description;
|
|
||||||
issue.workHours = workHours;
|
|
||||||
|
|
||||||
// 작업 시간이 입력되었으면 상태를 자동으로 '완료'로 변경
|
|
||||||
if (workHours > 0 && issue.status === 'new') {
|
|
||||||
issue.status = 'complete';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 저장
|
|
||||||
saveIssues();
|
|
||||||
|
|
||||||
// UI 업데이트
|
|
||||||
displayIssueList();
|
|
||||||
closeDetailModal();
|
|
||||||
|
|
||||||
// 성공 메시지
|
|
||||||
alert('저장되었습니다!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 보고서 생성
|
|
||||||
function generateReport() {
|
|
||||||
const container = document.getElementById('reportContent');
|
|
||||||
|
|
||||||
// 통계 계산
|
|
||||||
const totalHours = issues.reduce((sum, issue) => sum + issue.workHours, 0);
|
|
||||||
const categoryCount = {};
|
|
||||||
|
|
||||||
issues.forEach(issue => {
|
|
||||||
categoryCount[issue.category] = (categoryCount[issue.category] || 0) + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 날짜 범위
|
|
||||||
const dates = issues.map(i => new Date(i.reportDate));
|
|
||||||
const startDate = dates.length > 0 ? new Date(Math.min(...dates)) : new Date();
|
|
||||||
const endDate = new Date();
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="space-y-6">
|
|
||||||
<!-- 요약 페이지 -->
|
|
||||||
<div class="border-b pb-6">
|
|
||||||
<h3 class="text-2xl font-bold text-center mb-6">작업 보고서</h3>
|
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 gap-6">
|
|
||||||
<div class="bg-gray-50 rounded-lg p-4">
|
|
||||||
<h4 class="font-semibold text-gray-700 mb-3">작업 기간</h4>
|
|
||||||
<p class="text-lg">${startDate.toLocaleDateString()} ~ ${endDate.toLocaleDateString()}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-blue-50 rounded-lg p-4">
|
|
||||||
<h4 class="font-semibold text-blue-700 mb-3">총 공수</h4>
|
|
||||||
<p class="text-3xl font-bold text-blue-600">${totalHours}시간</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<h4 class="font-semibold text-gray-700 mb-3">카테고리별 분석</h4>
|
|
||||||
<div class="grid md:grid-cols-3 gap-4">
|
|
||||||
<div class="bg-blue-50 rounded-lg p-4 text-center">
|
|
||||||
<p class="text-2xl font-bold text-blue-600">${categoryCount.material_missing || 0}</p>
|
|
||||||
<p class="text-sm text-gray-600">자재누락</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-orange-50 rounded-lg p-4 text-center">
|
|
||||||
<p class="text-2xl font-bold text-orange-600">${categoryCount.dimension_defect || 0}</p>
|
|
||||||
<p class="text-sm text-gray-600">치수불량</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-purple-50 rounded-lg p-4 text-center">
|
|
||||||
<p class="text-2xl font-bold text-purple-600">${categoryCount.incoming_defect || 0}</p>
|
|
||||||
<p class="text-sm text-gray-600">입고자재 불량</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 상세 내역 -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-semibold text-gray-700 mb-4">부적합 사항 상세</h4>
|
|
||||||
${issues.map(issue => {
|
|
||||||
const categoryNames = {
|
|
||||||
material_missing: '자재누락',
|
|
||||||
dimension_defect: '치수불량',
|
|
||||||
incoming_defect: '입고자재 불량'
|
|
||||||
};
|
|
||||||
return `
|
|
||||||
<div class="border rounded-lg p-4 mb-4 break-inside-avoid">
|
|
||||||
<div class="flex gap-4">
|
|
||||||
${issue.photo ? `<img src="${issue.photo}" class="w-32 h-32 object-cover rounded-lg">` : ''}
|
|
||||||
<div class="flex-1">
|
|
||||||
<h5 class="font-semibold text-gray-800 mb-2">${categoryNames[issue.category] || issue.category}</h5>
|
|
||||||
<p class="text-gray-600 mb-2">${issue.description}</p>
|
|
||||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
|
||||||
<p><span class="font-medium">상태:</span> ${
|
|
||||||
issue.status === 'new' ? '신규' :
|
|
||||||
issue.status === 'progress' ? '진행중' : '완료'
|
|
||||||
}</p>
|
|
||||||
<p><span class="font-medium">작업시간:</span> ${issue.workHours}시간</p>
|
|
||||||
<p><span class="font-medium">보고자:</span> ${issue.reporterName}</p>
|
|
||||||
<p><span class="font-medium">보고일:</span> ${new Date(issue.reportDate).toLocaleDateString()}</p>
|
|
||||||
</div>
|
|
||||||
${issue.detailNotes ? `
|
|
||||||
<div class="mt-2 p-2 bg-gray-50 rounded">
|
|
||||||
<p class="text-sm text-gray-600">${issue.detailNotes}</p>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 인쇄
|
|
||||||
function printReport() {
|
|
||||||
window.print();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user