Files
M-Project/frontend/issues-dashboard.html
Hyungi Ahn 58156da987 🐛 Fix: Project.name → project_name 속성명 수정 및 보고서 시스템 안정화
- backend/routers/reports.py: project.name을 project.project_name으로 수정 (3곳)
- 일일보고서 엑셀 내보내기 오류 해결
- 배포 가이드 업데이트 (DEPLOYMENT_GUIDE_20251028.md)
- 프로젝트 속성명 불일치로 인한 500 에러 해결

Fixes: 'Project' object has no attribute 'name' 오류
2025-10-28 16:36:56 +09:00

1035 lines
48 KiB
HTML

<!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>
.fade-in { opacity: 0; animation: fadeIn 0.5s ease-in forwards; }
@keyframes fadeIn { to { opacity: 1; } }
.header-fade-in { opacity: 0; animation: headerFadeIn 0.6s ease-out forwards; }
@keyframes headerFadeIn { to { opacity: 1; transform: translateY(0); } from { transform: translateY(-10px); } }
.content-fade-in { opacity: 0; animation: contentFadeIn 0.7s ease-out 0.2s forwards; }
@keyframes contentFadeIn { to { opacity: 1; transform: translateY(0); } from { transform: translateY(20px); } }
/* 대시보드 카드 스타일 */
.dashboard-card {
transition: all 0.3s ease;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.dashboard-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* 이슈 카드 스타일 (세련된 모던 스타일) */
.issue-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-left: 4px solid transparent;
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
}
.issue-card:hover {
transform: translateY(-8px) scale(1.02);
border-left-color: #3b82f6;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(59, 130, 246, 0.1),
0 0 20px rgba(59, 130, 246, 0.1);
}
.issue-card label {
font-weight: 600;
color: #374151;
}
.issue-card .bg-gray-50 {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
transition: all 0.2s ease;
}
.issue-card .bg-gray-50:hover {
background-color: #f3f4f6;
}
.issue-card .fas.fa-image:hover {
transform: scale(1.2);
color: #3b82f6;
}
/* 진행 중 애니메이션 */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* 날짜 그룹 스타일 */
.date-group {
margin-bottom: 1.5rem;
}
.date-header {
cursor: pointer;
transition: all 0.2s ease;
}
.date-header:hover {
background-color: #f3f4f6 !important;
}
.collapse-content {
transition: all 0.3s ease;
}
.progress-bar {
background: linear-gradient(90deg, #10b981 0%, #059669 100%);
transition: width 0.8s ease;
}
/* 반응형 그리드 */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 로딩 스크린 -->
<div id="loadingScreen" class="fixed inset-0 bg-white z-50 flex items-center justify-center">
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p class="text-gray-600">현황판을 불러오는 중...</p>
</div>
</div>
<!-- 로그인 스크린 -->
<div id="loginScreen" class="hidden fixed inset-0 bg-gray-100 z-40 flex items-center justify-center">
<div class="bg-white p-8 rounded-xl shadow-lg max-w-md w-full mx-4">
<h2 class="text-2xl font-bold text-center mb-6 text-gray-800">로그인</h2>
<form id="loginForm">
<div class="mb-4">
<input type="text" id="username" placeholder="사용자명" required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="mb-6">
<input type="password" id="password" placeholder="비밀번호" required
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition-colors">
로그인
</button>
</form>
</div>
</div>
<!-- 메인 콘텐츠 -->
<div id="mainContent" class="min-h-screen">
<!-- 공통 헤더 -->
<div id="commonHeader"></div>
<!-- 메인 콘텐츠 -->
<main class="container mx-auto px-4 py-8 content-fade-in" style="padding-top: 80px;">
<!-- 페이지 헤더 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
<i class="fas fa-chart-line text-blue-500 mr-3"></i>
부적합 현황판
</h1>
<p class="text-gray-600 mt-1">진행 중인 부적합 사항을 프로젝트별로 한눈에 확인하세요</p>
</div>
</div>
</div>
<!-- 전체 통계 대시보드 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="dashboard-card text-white p-6 rounded-xl">
<div class="flex items-center justify-between">
<div>
<p class="text-blue-100 text-sm flex items-center space-x-1">
<span>전체 진행 중</span>
<div class="w-1.5 h-1.5 bg-blue-200 rounded-full animate-pulse"></div>
</p>
<p class="text-3xl font-bold" id="totalInProgress">0</p>
</div>
<i class="fas fa-tasks text-4xl text-blue-200"></i>
</div>
</div>
<div class="bg-gradient-to-br from-green-400 to-green-600 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between">
<div>
<p class="text-green-100 text-sm flex items-center space-x-1">
<span>오늘 신규</span>
<div class="w-1.5 h-1.5 bg-green-200 rounded-full animate-pulse"></div>
</p>
<p class="text-3xl font-bold" id="todayNew">0</p>
</div>
<i class="fas fa-plus-circle text-4xl text-green-200"></i>
</div>
</div>
<div class="bg-gradient-to-br from-purple-400 to-purple-600 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between">
<div>
<p class="text-purple-100 text-sm flex items-center space-x-1">
<span>완료 대기</span>
<div class="w-1.5 h-1.5 bg-purple-200 rounded-full animate-pulse"></div>
</p>
<p class="text-3xl font-bold" id="pendingCompletion">0</p>
</div>
<i class="fas fa-hourglass-half text-4xl text-purple-200"></i>
</div>
</div>
<div class="bg-gradient-to-br from-red-400 to-red-600 text-white p-6 rounded-xl dashboard-card">
<div class="flex items-center justify-between">
<div>
<p class="text-red-100 text-sm flex items-center space-x-1">
<span>지연 중</span>
<div class="w-1.5 h-1.5 bg-red-200 rounded-full animate-pulse"></div>
</p>
<p class="text-3xl font-bold" id="overdue">0</p>
</div>
<i class="fas fa-clock text-4xl text-red-200"></i>
</div>
</div>
</div>
<!-- 프로젝트 선택 및 필터 -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div class="flex items-center space-x-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">프로젝트 선택</label>
<select id="projectFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-[200px]" onchange="filterByProject()">
<option value="">전체 프로젝트</option>
</select>
</div>
</div>
<div class="flex items-center space-x-2">
<button onclick="refreshDashboard()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<i class="fas fa-sync-alt mr-2"></i>새로고침
</button>
</div>
</div>
</div>
<!-- 프로젝트별 현황 -->
<div id="projectDashboard" class="space-y-6">
<!-- 동적으로 생성될 프로젝트 카드들 -->
</div>
<!-- 빈 상태 -->
<div id="emptyState" class="hidden text-center py-12">
<i class="fas fa-chart-line text-6xl text-gray-300 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 mb-2">진행 중인 부적합이 없습니다</h3>
<p class="text-gray-500">새로운 부적합이 등록되면 여기에 표시됩니다.</p>
</div>
</main>
</div>
<!-- 스크립트 -->
<script src="/static/js/core/permissions.js?v=1.1"></script>
<script src="/static/js/components/common-header.js?v=1.1"></script>
<script src="/static/js/core/page-manager.js?v=1.1"></script>
<script src="/static/js/core/auth-manager.js?v=1.1"></script>
<script>
let currentUser = null;
let allIssues = [];
let projects = [];
let filteredIssues = [];
// 애니메이션 함수들
function animateHeaderAppearance() {
const header = document.getElementById('commonHeader');
if (header) {
header.classList.add('header-fade-in');
}
}
function animateContentAppearance() {
const content = document.querySelector('.content-fade-in');
if (content) {
content.classList.add('visible');
}
}
// 페이지 초기화
async function initializeDashboard() {
try {
// 인증 확인
currentUser = await window.authManager.checkAuth();
if (!currentUser) {
showLoginScreen();
return;
}
// 페이지 권한 확인
window.pagePermissionManager.setUser(currentUser);
await window.pagePermissionManager.loadPagePermissions();
if (!window.pagePermissionManager.canAccessPage('issues_dashboard')) {
alert('현황판 접근 권한이 없습니다.');
window.location.href = '/';
return;
}
// 공통 헤더 초기화
if (window.commonHeader) {
await window.commonHeader.init(currentUser, 'issues_dashboard');
setTimeout(() => animateHeaderAppearance(), 100);
}
// 데이터 로드
await Promise.all([
loadProjects(),
loadInProgressIssues()
]);
updateDashboard();
hideLoadingScreen();
} catch (error) {
console.error('대시보드 초기화 실패:', error);
alert('대시보드를 불러오는데 실패했습니다.');
hideLoadingScreen();
}
}
// 로딩 스크린 관리
function hideLoadingScreen() {
document.getElementById('loadingScreen').style.display = 'none';
}
function showLoginScreen() {
document.getElementById('loadingScreen').style.display = 'none';
document.getElementById('loginScreen').classList.remove('hidden');
}
// 데이터 로드 함수들
async function loadProjects() {
try {
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
const response = await fetch(`${apiUrl}/projects/`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
projects = await response.json();
updateProjectFilter();
} else {
throw new Error('프로젝트 목록을 불러올 수 없습니다.');
}
} catch (error) {
console.error('프로젝트 로드 실패:', error);
}
}
async function loadInProgressIssues() {
try {
const response = await fetch('/api/issues/admin/all', {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const allData = await response.json();
// 진행 중 상태만 필터링
allIssues = allData.filter(issue => issue.review_status === 'in_progress');
filteredIssues = [...allIssues];
} else {
throw new Error('부적합 목록을 불러올 수 없습니다.');
}
} catch (error) {
console.error('부적합 로드 실패:', error);
}
}
// 프로젝트 필터 업데이트
function updateProjectFilter() {
const projectFilter = document.getElementById('projectFilter');
projectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
projects.forEach(project => {
const option = document.createElement('option');
option.value = project.id;
option.textContent = project.project_name;
projectFilter.appendChild(option);
});
}
// 대시보드 업데이트
function updateDashboard() {
updateStatistics();
updateProjectCards();
}
// 통계 업데이트
function updateStatistics() {
const today = new Date().toDateString();
// 오늘 신규 (오늘 수신함에서 진행중으로 넘어온 것들)
const todayIssues = allIssues.filter(issue =>
issue.reviewed_at && new Date(issue.reviewed_at).toDateString() === today
);
// 완료 대기 (완료 신청이 된 것들)
const pendingCompletionIssues = allIssues.filter(issue =>
issue.completion_requested_at && issue.review_status === 'in_progress'
);
// 지연 중 (마감일이 지난 것들)
const overdueIssues = allIssues.filter(issue => {
if (!issue.expected_completion_date) return false;
const expectedDate = new Date(issue.expected_completion_date);
const now = new Date();
return expectedDate < now; // 마감일 지남
});
document.getElementById('totalInProgress').textContent = allIssues.length;
document.getElementById('todayNew').textContent = todayIssues.length;
document.getElementById('pendingCompletion').textContent = pendingCompletionIssues.length;
document.getElementById('overdue').textContent = overdueIssues.length;
}
// 이슈 카드 업데이트 (관리함 스타일 - 날짜별 그룹화)
function updateProjectCards() {
const container = document.getElementById('projectDashboard');
const emptyState = document.getElementById('emptyState');
if (filteredIssues.length === 0) {
container.innerHTML = '';
emptyState.classList.remove('hidden');
return;
}
emptyState.classList.add('hidden');
// 날짜별로 그룹화 (관리함 진입일 기준)
const groupedByDate = {};
const dateObjects = {}; // 정렬용 Date 객체 저장
filteredIssues.forEach(issue => {
// reviewed_at이 있으면 관리함 진입일, 없으면 report_date 사용
const dateToUse = issue.reviewed_at || issue.report_date;
const dateObj = new Date(dateToUse);
const dateKey = dateObj.toLocaleDateString('ko-KR');
if (!groupedByDate[dateKey]) {
groupedByDate[dateKey] = [];
dateObjects[dateKey] = dateObj;
}
groupedByDate[dateKey].push(issue);
});
// 날짜별 그룹 생성
const dateGroups = Object.keys(groupedByDate)
.sort((a, b) => dateObjects[b] - dateObjects[a]) // 최신순
.map(dateKey => {
const issues = groupedByDate[dateKey];
const formattedDate = dateObjects[dateKey].toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\./g, '. ').trim();
return `
<div class="date-group mb-6">
<div class="date-header flex items-center justify-between p-4 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100 transition-colors" onclick="toggleDateGroup('${dateKey}')">
<div class="flex items-center space-x-3">
<i class="fas fa-chevron-down transition-transform duration-200" id="chevron-${dateKey}"></i>
<span class="font-semibold text-gray-800">${formattedDate}</span>
<span class="text-sm text-gray-500">(${issues.length}건)</span>
<span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">관리함 진입일</span>
</div>
</div>
<div class="collapse-content mt-4" id="content-${dateKey}">
<div class="grid grid-cols-1 gap-4">
${issues.map(issue => createIssueCard(issue)).join('')}
</div>
</div>
</div>
`;
}).join('');
container.innerHTML = dateGroups;
}
// 부적합명 추출 (첫 번째 줄)
function getIssueTitle(issue) {
const description = issue.description || issue.final_description || '';
const lines = description.split('\n');
return lines[0] || '부적합명 없음';
}
// 상세 내용 추출 (두 번째 줄부터)
function getIssueDetail(issue) {
const description = issue.description || issue.final_description || '';
const lines = description.split('\n');
return lines.slice(1).join('\n') || '상세 내용 없음';
}
// 이슈 카드 생성 (관리함 진행 중 스타일, 읽기 전용)
function createIssueCard(issue) {
const project = projects.find(p => p.id === issue.project_id);
const projectName = project ? project.project_name : '미지정';
// 부서 텍스트 변환
const getDepartmentText = (dept) => {
const deptMap = {
'production': '생산',
'quality': '품질',
'purchasing': '구매',
'design': '설계',
'sales': '영업'
};
return dept ? deptMap[dept] || dept : '-';
};
// 카테고리 텍스트 변환
const getCategoryText = (category) => {
const categoryMap = {
'material_missing': '자재 누락',
'design_error': '설계 오류',
'incoming_defect': '반입 불량',
'inspection_miss': '검사 누락',
'etc': '기타'
};
return category ? categoryMap[category] || category : '-';
};
// 날짜 포맷팅
const formatKSTDate = (dateStr) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
};
// 상태 체크 함수들
const getIssueStatus = () => {
if (issue.review_status === 'completed') return 'completed';
if (issue.completion_requested_at) return 'pending_completion'; // 완료 대기
if (issue.expected_completion_date) {
const expectedDate = new Date(issue.expected_completion_date);
const now = new Date();
const diffDays = (expectedDate - now) / (1000 * 60 * 60 * 24);
if (diffDays < 0) return 'overdue'; // 지연됨
if (diffDays <= 3) return 'urgent'; // 긴급
}
return 'in_progress'; // 진행 중
};
const getStatusConfig = (status) => {
const configs = {
'in_progress': {
text: '진행 중',
bgColor: 'bg-gradient-to-r from-blue-500 to-blue-600',
icon: 'fas fa-cog fa-spin',
dotColor: 'bg-white'
},
'urgent': {
text: '긴급',
bgColor: 'bg-gradient-to-r from-orange-500 to-orange-600',
icon: 'fas fa-exclamation-triangle',
dotColor: 'bg-white'
},
'overdue': {
text: '지연됨',
bgColor: 'bg-gradient-to-r from-red-500 to-red-600',
icon: 'fas fa-clock',
dotColor: 'bg-white'
},
'pending_completion': {
text: '완료 대기',
bgColor: 'bg-gradient-to-r from-purple-500 to-purple-600',
icon: 'fas fa-hourglass-half',
dotColor: 'bg-white'
},
'completed': {
text: '완료됨',
bgColor: 'bg-gradient-to-r from-green-500 to-green-600',
icon: 'fas fa-check-circle',
dotColor: 'bg-white'
}
};
return configs[status] || configs['in_progress'];
};
const currentStatus = getIssueStatus();
const statusConfig = getStatusConfig(currentStatus);
const isUrgent = () => currentStatus === 'urgent';
return `
<div class="issue-card bg-white rounded-xl border border-gray-200 p-5 hover:shadow-xl hover:border-blue-300 transition-all duration-300 transform hover:-translate-y-1">
<!-- 헤더 -->
<div class="flex justify-between items-start mb-4">
<div class="flex flex-col space-y-2 flex-1">
<div class="flex items-center space-x-2">
<span class="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent">No.${issue.project_sequence_no || '-'}</span>
<div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse shadow-sm"></div>
<span class="text-sm text-gray-600">${projectName}</span>
</div>
<div class="bg-blue-50 px-3 py-1.5 rounded-lg border-l-4 border-blue-400">
<h3 class="text-base font-bold text-blue-900">${getIssueTitle(issue)}</h3>
</div>
</div>
<div class="flex flex-col items-end space-y-2 ml-4">
<div class="flex items-center space-x-3">
<div class="flex items-center space-x-2 ${statusConfig.bgColor} text-white px-4 py-1.5 rounded-full shadow-md">
<div class="w-2 h-2 ${statusConfig.dotColor} rounded-full animate-pulse"></div>
<span class="text-xs font-bold">${statusConfig.text}</span>
<i class="${statusConfig.icon} text-xs"></i>
</div>
<div class="bg-gray-100 px-3 py-1 rounded-full">
<span class="text-xs text-gray-600 font-medium">발생: ${formatKSTDate(issue.report_date)}</span>
</div>
</div>
<div class="flex items-center space-x-2">
<span class="inline-flex items-center bg-gradient-to-r from-yellow-400 to-orange-400 text-white px-2 py-1 rounded-full text-xs font-medium shadow-sm">
<i class="fas fa-tag mr-1"></i>
${getCategoryText(issue.category || issue.final_category)}
</span>
</div>
</div>
</div>
<!-- 2x2 그리드 -->
<div class="grid grid-cols-2 gap-4 text-sm">
<!-- 첫 번째 행: 상세 내용 | 해결 방안 -->
<div>
<div class="flex items-center mb-2">
<i class="fas fa-align-left text-gray-500 mr-2"></i>
<span class="text-gray-600 font-medium text-sm">상세 내용</span>
</div>
<div class="bg-gray-50 rounded-lg p-3 border border-gray-200 min-h-[80px]">
<div class="text-gray-600 text-sm leading-relaxed italic">
${getIssueDetail(issue)}
</div>
</div>
</div>
<div>
<div class="flex items-center mb-2">
<i class="fas fa-lightbulb text-green-500 mr-2"></i>
<span class="text-gray-600 font-medium text-sm">해결 방안</span>
</div>
<div class="bg-green-50 rounded-lg p-3 border border-green-200 min-h-[80px]">
<div class="text-green-700 text-sm leading-relaxed">
${issue.solution || '<span class="text-gray-400 italic">미정</span>'}
</div>
</div>
</div>
<!-- 두 번째 행: 이미지 | 마감시간/담당자 -->
<div>
<div class="flex items-center mb-2">
<i class="fas fa-camera text-blue-500 mr-2"></i>
<span class="text-gray-600 font-medium text-sm">이미지</span>
</div>
<div class="flex space-x-2">
${issue.photo_path ? `
<div class="relative w-24 h-24 rounded-lg border-2 border-blue-200 overflow-hidden cursor-pointer hover:shadow-lg hover:scale-105 transition-all duration-200" onclick="openPhotoModal('${issue.photo_path}')">
<img src="${issue.photo_path}" alt="부적합 사진 1" class="w-full h-full object-cover" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="absolute inset-0 bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center" style="display: none;">
<i class="fas fa-image text-blue-500 text-xl"></i>
</div>
<div class="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white"></div>
</div>
` : `
<div class="w-24 h-24 bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border-2 border-gray-200 flex items-center justify-center">
<i class="fas fa-image text-gray-400 text-xl opacity-50"></i>
</div>
`}
${issue.photo_path2 ? `
<div class="relative w-24 h-24 rounded-lg border-2 border-blue-200 overflow-hidden cursor-pointer hover:shadow-lg hover:scale-105 transition-all duration-200" onclick="openPhotoModal('${issue.photo_path2}')">
<img src="${issue.photo_path2}" alt="부적합 사진 2" class="w-full h-full object-cover" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="absolute inset-0 bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center" style="display: none;">
<i class="fas fa-image text-blue-500 text-xl"></i>
</div>
<div class="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white"></div>
</div>
` : `
<div class="w-24 h-24 bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg border-2 border-gray-200 flex items-center justify-center">
<i class="fas fa-image text-gray-400 text-xl opacity-50"></i>
</div>
`}
</div>
</div>
<div>
<div class="flex items-center mb-2">
<i class="fas fa-user-clock text-purple-500 mr-2"></i>
<span class="text-gray-600 font-medium text-sm">일정 & 담당</span>
</div>
<div class="space-y-3">
<div class="bg-purple-50 rounded-lg p-3 border border-purple-200">
<div class="flex items-center justify-between">
<span class="text-purple-600 text-xs font-medium">담당자</span>
<i class="fas fa-user text-purple-500 text-xs"></i>
</div>
<div class="text-purple-800 font-semibold text-sm mt-1">${issue.responsible_person || '-'}</div>
</div>
<div class="bg-orange-50 rounded-lg p-3 border border-orange-200 relative">
<div class="flex items-center justify-between">
<span class="text-orange-600 text-xs font-medium">마감시간</span>
<i class="fas fa-calendar-alt text-orange-500 text-xs"></i>
</div>
<div class="text-orange-800 font-semibold text-sm mt-1">${formatKSTDate(issue.expected_completion_date)}</div>
${currentStatus === 'in_progress' || currentStatus === 'urgent' || currentStatus === 'overdue' ? `
<button onclick="openCompletionRequestModal(${issue.id})"
class="absolute bottom-1 right-1 bg-green-500 hover:bg-green-600 text-white text-xs px-2 py-1 rounded-full transition-colors shadow-sm">
<i class="fas fa-check text-xs mr-1"></i>완료신청
</button>
` : ''}
</div>
</div>
</div>
</div>
</div>
`;
}
// 사진 모달 열기
function openPhotoModal(photoPath) {
let modal = document.getElementById('photoModal');
if (!modal) {
// 모달이 없으면 생성
const modalHTML = `
<div id="photoModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="relative max-w-4xl max-h-full p-4">
<img id="modalPhoto" src="" alt="부적합 사진" class="max-w-full max-h-full object-contain rounded-lg">
<button onclick="closePhotoModal()" class="absolute top-4 right-4 text-white text-2xl hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
modal = document.getElementById('photoModal');
}
document.getElementById('modalPhoto').src = photoPath;
modal.classList.remove('hidden');
}
// 사진 모달 닫기
function closePhotoModal() {
const modal = document.getElementById('photoModal');
if (modal) {
modal.classList.add('hidden');
}
}
// ESC 키로 모달 닫기
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePhotoModal();
}
});
// 날짜 그룹 토글 기능
function toggleDateGroup(dateKey) {
const content = document.getElementById(`content-${dateKey}`);
const chevron = document.getElementById(`chevron-${dateKey}`);
if (content.style.display === 'none') {
content.style.display = 'block';
chevron.classList.remove('fa-chevron-right');
chevron.classList.add('fa-chevron-down');
} else {
content.style.display = 'none';
chevron.classList.remove('fa-chevron-down');
chevron.classList.add('fa-chevron-right');
}
}
// 필터 및 정렬 함수들
function filterByProject() {
const projectId = document.getElementById('projectFilter').value;
if (projectId) {
filteredIssues = allIssues.filter(issue => issue.project_id == projectId);
} else {
filteredIssues = [...allIssues];
}
updateProjectCards();
}
// 유틸리티 함수들
function getDepartmentText(department) {
const departments = {
'production': '생산',
'quality': '품질',
'purchasing': '구매',
'design': '설계',
'sales': '영업'
};
return departments[department] || department;
}
function viewIssueDetail(issueId) {
window.location.href = `/issues-management.html#issue-${issueId}`;
}
function refreshDashboard() {
location.reload();
}
// 로그인 처리
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const user = await window.authManager.login(username, password);
if (user) {
document.getElementById('loginScreen').classList.add('hidden');
await initializeDashboard();
}
} catch (error) {
alert('로그인에 실패했습니다.');
}
});
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', () => {
// AuthManager 로드 대기
const checkAuthManager = () => {
if (window.authManager) {
initializeDashboard();
} else {
setTimeout(checkAuthManager, 100);
}
};
checkAuthManager();
});
</script>
<!-- API 스크립트 동적 로드 -->
<script>
function initializeDashboardApp() {
console.log('✅ API 스크립트 로드 완료 (issues-dashboard.html)');
}
// 현황판 새로고침 함수
function refreshDashboard() {
// 현재 선택된 프로젝트 기준으로 다시 로드
const selectedProject = document.getElementById('projectFilter').value;
if (selectedProject) {
filterByProject();
} else {
initializeDashboard();
}
}
// 완료 신청 관련 함수들
let selectedCompletionIssueId = null;
let completionPhotoBase64 = null;
function openCompletionRequestModal(issueId) {
selectedCompletionIssueId = issueId;
document.getElementById('completionRequestModal').classList.remove('hidden');
// 폼 초기화
document.getElementById('completionRequestForm').reset();
document.getElementById('photoPreview').classList.add('hidden');
document.getElementById('photoUploadArea').classList.remove('hidden');
completionPhotoBase64 = null;
}
function closeCompletionRequestModal() {
selectedCompletionIssueId = null;
completionPhotoBase64 = null;
document.getElementById('completionRequestModal').classList.add('hidden');
}
function handleCompletionPhotoUpload(event) {
const file = event.target.files[0];
if (!file) return;
// 파일 크기 체크 (5MB 제한)
if (file.size > 5 * 1024 * 1024) {
alert('파일 크기는 5MB 이하여야 합니다.');
return;
}
// 이미지 파일 체크
if (!file.type.startsWith('image/')) {
alert('이미지 파일만 업로드 가능합니다.');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
completionPhotoBase64 = e.target.result;
// 미리보기 표시
document.getElementById('previewImage').src = e.target.result;
document.getElementById('photoUploadArea').classList.add('hidden');
document.getElementById('photoPreview').classList.remove('hidden');
};
reader.readAsDataURL(file);
}
// 완료 신청 폼 제출 처리
document.addEventListener('DOMContentLoaded', function() {
const completionForm = document.getElementById('completionRequestForm');
if (completionForm) {
completionForm.addEventListener('submit', async function(e) {
e.preventDefault();
if (!selectedCompletionIssueId) {
alert('선택된 이슈가 없습니다.');
return;
}
if (!completionPhotoBase64) {
alert('완료 사진을 업로드해주세요.');
return;
}
const comment = document.getElementById('completionComment').value.trim();
try {
const response = await fetch(`/api/issues/${selectedCompletionIssueId}/completion-request`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
completion_photo: completionPhotoBase64,
completion_comment: comment
})
});
if (response.ok) {
alert('완료 신청이 성공적으로 제출되었습니다.');
closeCompletionRequestModal();
// 현황판 새로고침
refreshDashboard();
} else {
const error = await response.json();
alert(`완료 신청 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('완료 신청 오류:', error);
alert('완료 신청 중 오류가 발생했습니다.');
}
});
}
});
// API 스크립트 동적 로드
const script = document.createElement('script');
script.src = '/static/js/api.js?v=' + Date.now();
script.onload = initializeDashboardApp;
document.body.appendChild(script);
</script>
<!-- 완료 신청 모달 -->
<div id="completionRequestModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
<div class="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="p-6">
<!-- 모달 헤더 -->
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold text-gray-900">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
완료 신청
</h3>
<button onclick="closeCompletionRequestModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 모달 내용 -->
<form id="completionRequestForm" class="space-y-4">
<!-- 완료 사진 업로드 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-camera text-green-500 mr-1"></i>
완료 사진 (필수)
</label>
<div class="border-2 border-dashed border-gray-300 rounded-lg p-4 text-center hover:border-green-400 transition-colors">
<input type="file" id="completionPhoto" accept="image/*" class="hidden" onchange="handleCompletionPhotoUpload(event)">
<div id="photoUploadArea" onclick="document.getElementById('completionPhoto').click()" class="cursor-pointer">
<i class="fas fa-cloud-upload-alt text-gray-400 text-2xl mb-2"></i>
<p class="text-sm text-gray-600">클릭하여 완료 사진을 업로드하세요</p>
<p class="text-xs text-gray-500 mt-1">JPG, PNG 파일만 가능</p>
</div>
<div id="photoPreview" class="hidden mt-3">
<img id="previewImage" class="max-w-full h-32 object-cover rounded-lg mx-auto">
<p class="text-sm text-green-600 mt-2">✓ 사진이 업로드되었습니다</p>
</div>
</div>
</div>
<!-- 완료 코멘트 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">
<i class="fas fa-comment text-blue-500 mr-1"></i>
완료 코멘트 (선택사항)
</label>
<textarea id="completionComment" rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 resize-none"
placeholder="완료 상황에 대한 간단한 설명을 입력하세요..."></textarea>
</div>
<!-- 안내 메시지 -->
<div class="bg-green-50 border border-green-200 rounded-lg p-3">
<div class="flex items-start">
<i class="fas fa-info-circle text-green-500 mt-0.5 mr-2"></i>
<div class="text-sm text-green-700">
<p class="font-medium mb-1">완료 신청 안내</p>
<p>완료 사진과 함께 신청하시면 관리자 승인 후 완료 처리됩니다.</p>
</div>
</div>
</div>
<!-- 버튼 -->
<div class="flex space-x-3 pt-4">
<button type="button" onclick="closeCompletionRequestModal()"
class="flex-1 px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
취소
</button>
<button type="submit"
class="flex-1 px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-paper-plane mr-2"></i>완료 신청
</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>