🔄 Complete Layout Change: - ❌ 단순 카드 그리드 → ✅ 관리함 스타일 날짜별 그룹화 - 날짜별 접기/펼치기 기능 추가 - 업로드일 기준으로 그룹화 및 최신순 정렬 📋 Card Layout (Management Style): - 헤더: No., 긴급 표시 + 저장/완료처리 버튼 - 업로드 사진: 맨 위로 이동 (2개 슬롯, 없으면 '사진 없음') - 부적합 내용: 읽기 전용 텍스트 - 원인 분류: 노란색 배지로 표시 - 관리 정보: 편집 가능한 입력 필드들 🎯 Interactive Features: - 해결방안: textarea (편집 가능) - 담당부서: select dropdown (편집 가능) - 담당자: text input (편집 가능) - 조치 예상일: date input (편집 가능) - 원인부서: 읽기 전용 📅 Date Grouping: - 업로드일(report_date) 기준 그룹화 - 날짜 헤더: 클릭으로 접기/펼치기 - 각 그룹별 건수 표시 - '업로드일' 배지로 기준 명시 🎨 Visual Improvements: - 이모지 아이콘으로 필드 구분 (💡🏢👤📅) - 진행 중 상태 애니메이션 유지 - 부드러운 hover 효과 - 일관된 색상 체계 🔧 Functionality: - toggleDateGroup() 함수로 그룹 접기/펼치기 - 사진 모달 확대 기능 유지 - 긴급 표시 (3일 이내 마감) - 저장/완료처리 버튼 (UI만, 기능은 추후) Expected Result: ✅ 관리함과 완전히 동일한 UI/UX ✅ 날짜별 체계적 정리 ✅ 편집 가능한 관리 인터페이스 ✅ 직관적인 정보 표시
786 lines
36 KiB
HTML
786 lines
36 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 ease;
|
|
border-left: 4px solid transparent;
|
|
}
|
|
|
|
.issue-card:hover {
|
|
transform: translateY(-4px);
|
|
border-left-color: #3b82f6;
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.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">전체 진행 중</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">오늘 신규</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-yellow-400 to-orange-500 text-white p-6 rounded-xl dashboard-card">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-yellow-100 text-sm">지연 위험</p>
|
|
<p class="text-3xl font-bold" id="delayRisk">0</p>
|
|
</div>
|
|
<i class="fas fa-exclamation-triangle text-4xl text-yellow-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">활성 프로젝트</p>
|
|
<p class="text-3xl font-bold" id="activeProjects">0</p>
|
|
</div>
|
|
<i class="fas fa-project-diagram text-4xl text-purple-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>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">정렬 기준</label>
|
|
<select id="sortOrder" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" onchange="sortIssues()">
|
|
<option value="priority">우선순위</option>
|
|
<option value="date">신고일순</option>
|
|
<option value="deadline">마감일순</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 response = await fetch('/api/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 =>
|
|
new Date(issue.report_date).toDateString() === today
|
|
);
|
|
|
|
// 지연 위험 계산 (예상일이 지났거나 3일 이내)
|
|
const delayRiskIssues = allIssues.filter(issue => {
|
|
if (!issue.expected_completion_date) return false;
|
|
const expectedDate = new Date(issue.expected_completion_date);
|
|
const now = new Date();
|
|
const diffDays = (expectedDate - now) / (1000 * 60 * 60 * 24);
|
|
return diffDays <= 3; // 3일 이내 또는 지연
|
|
});
|
|
|
|
// 활성 프로젝트 (진행 중인 부적합이 있는 프로젝트)
|
|
const activeProjectIds = new Set(allIssues.map(issue => issue.project_id));
|
|
|
|
document.getElementById('totalInProgress').textContent = allIssues.length;
|
|
document.getElementById('todayNew').textContent = todayIssues.length;
|
|
document.getElementById('delayRisk').textContent = delayRiskIssues.length;
|
|
document.getElementById('activeProjects').textContent = activeProjectIds.size;
|
|
}
|
|
|
|
// 이슈 카드 업데이트 (관리함 스타일 - 날짜별 그룹화)
|
|
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 = {};
|
|
filteredIssues.forEach(issue => {
|
|
const dateKey = new Date(issue.report_date).toLocaleDateString('ko-KR');
|
|
if (!groupedByDate[dateKey]) {
|
|
groupedByDate[dateKey] = [];
|
|
}
|
|
groupedByDate[dateKey].push(issue);
|
|
});
|
|
|
|
// 날짜별 그룹 생성
|
|
const dateGroups = Object.keys(groupedByDate)
|
|
.sort((a, b) => new Date(b) - new Date(a)) // 최신순
|
|
.map(dateKey => {
|
|
const issues = groupedByDate[dateKey];
|
|
const formattedDate = new Date(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-blue-100 text-blue-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 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 = {
|
|
'quality': '품질',
|
|
'safety': '안전',
|
|
'environment': '환경',
|
|
'process': '공정',
|
|
'equipment': '장비',
|
|
'material': '자재',
|
|
'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 isUrgent = () => {
|
|
if (!issue.expected_completion_date) return false;
|
|
const expectedDate = new Date(issue.expected_completion_date);
|
|
const now = new Date();
|
|
const diffDays = (expectedDate - now) / (1000 * 60 * 60 * 24);
|
|
return diffDays <= 3; // 3일 이내 또는 지연
|
|
};
|
|
|
|
return `
|
|
<div class="issue-card bg-white rounded-lg border border-gray-200 p-6 hover:shadow-md transition-all duration-200">
|
|
<!-- 헤더 -->
|
|
<div class="flex justify-between items-start mb-4">
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-lg font-bold text-blue-600">No.${issue.project_sequence_no || '-'}</span>
|
|
${isUrgent() ? '<span class="bg-red-100 text-red-800 text-xs font-medium px-2 py-1 rounded">긴급</span>' : ''}
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<button class="bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600 transition-colors">
|
|
<i class="fas fa-edit mr-1"></i>저장
|
|
</button>
|
|
<button class="bg-green-500 text-white px-3 py-1 rounded text-sm hover:bg-green-600 transition-colors">
|
|
<i class="fas fa-check mr-1"></i>완료처리
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 업로드 사진 (맨 위로) -->
|
|
${issue.photo_path || issue.photo_path2 ? `
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">업로드 사진</label>
|
|
<div class="flex space-x-2">
|
|
${issue.photo_path ? `
|
|
<div class="w-20 h-20 bg-gray-100 rounded border flex items-center justify-center cursor-pointer hover:bg-gray-200 transition-colors" onclick="openPhotoModal('${issue.photo_path}')">
|
|
<i class="fas fa-image text-gray-400"></i>
|
|
</div>
|
|
` : `
|
|
<div class="w-20 h-20 bg-gray-100 rounded border flex items-center justify-center">
|
|
<span class="text-xs text-gray-400">사진 없음</span>
|
|
</div>
|
|
`}
|
|
${issue.photo_path2 ? `
|
|
<div class="w-20 h-20 bg-gray-100 rounded border flex items-center justify-center cursor-pointer hover:bg-gray-200 transition-colors" onclick="openPhotoModal('${issue.photo_path2}')">
|
|
<i class="fas fa-image text-gray-400"></i>
|
|
</div>
|
|
` : `
|
|
<div class="w-20 h-20 bg-gray-100 rounded border flex items-center justify-center">
|
|
<span class="text-xs text-gray-400">사진 없음</span>
|
|
</div>
|
|
`}
|
|
</div>
|
|
</div>
|
|
` : `
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">업로드 사진</label>
|
|
<div class="flex space-x-2">
|
|
<div class="w-20 h-20 bg-gray-100 rounded border flex items-center justify-center">
|
|
<span class="text-xs text-gray-400">사진 없음</span>
|
|
</div>
|
|
<div class="w-20 h-20 bg-gray-100 rounded border flex items-center justify-center">
|
|
<span class="text-xs text-gray-400">사진 없음</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`}
|
|
|
|
<!-- 부적합 내용 -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">부적합 내용</label>
|
|
<div class="text-sm text-gray-900">${issue.final_description || issue.description || '중복작업 신고용'}</div>
|
|
</div>
|
|
|
|
<!-- 원인 분류 -->
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">원인 분류</label>
|
|
<span class="inline-block bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm">${getCategoryText(issue.final_category || issue.category)}</span>
|
|
</div>
|
|
|
|
<!-- 관리 정보 -->
|
|
<div class="space-y-3 border-t pt-4">
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">💡 해결방안</label>
|
|
<textarea class="w-full text-sm border border-gray-300 rounded px-3 py-2 resize-none" rows="2" placeholder="해결 방안을 입력하세요...">${issue.solution || ''}</textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">🏢 담당부서</label>
|
|
<select class="w-full text-sm border border-gray-300 rounded px-3 py-2">
|
|
<option value="">선택하세요</option>
|
|
<option value="production" ${issue.responsible_department === 'production' ? 'selected' : ''}>생산</option>
|
|
<option value="quality" ${issue.responsible_department === 'quality' ? 'selected' : ''}>품질</option>
|
|
<option value="purchasing" ${issue.responsible_department === 'purchasing' ? 'selected' : ''}>구매</option>
|
|
<option value="design" ${issue.responsible_department === 'design' ? 'selected' : ''}>설계</option>
|
|
<option value="sales" ${issue.responsible_department === 'sales' ? 'selected' : ''}>영업</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">👤 담당자</label>
|
|
<input type="text" class="w-full text-sm border border-gray-300 rounded px-3 py-2" placeholder="담당자 이름" value="${issue.responsible_person || ''}">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">📅 조치 예상일</label>
|
|
<input type="date" class="w-full text-sm border border-gray-300 rounded px-3 py-2" value="${issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : '2025-10-26'}">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">원인부서</label>
|
|
<div class="text-sm text-gray-900">${getDepartmentText(issue.cause_department)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 진행 중 표시 -->
|
|
<div class="border-t pt-4 mt-4 flex items-center justify-between">
|
|
<div class="flex items-center space-x-2">
|
|
<div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
|
|
<span class="text-sm font-medium text-blue-600">진행 중</span>
|
|
</div>
|
|
<span class="text-xs text-gray-500">신고일: ${formatKSTDate(issue.report_date)}</span>
|
|
</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];
|
|
}
|
|
|
|
sortIssues();
|
|
updateProjectCards();
|
|
}
|
|
|
|
function sortIssues() {
|
|
const sortOrder = document.getElementById('sortOrder').value;
|
|
|
|
filteredIssues.sort((a, b) => {
|
|
switch (sortOrder) {
|
|
case 'priority':
|
|
// 긴급도 우선 (마감일이 가까운 순)
|
|
const aUrgent = a.expected_completion_date ?
|
|
(new Date(a.expected_completion_date) - new Date()) / (1000 * 60 * 60 * 24) : 999;
|
|
const bUrgent = b.expected_completion_date ?
|
|
(new Date(b.expected_completion_date) - new Date()) / (1000 * 60 * 60 * 24) : 999;
|
|
return aUrgent - bUrgent;
|
|
case 'date':
|
|
return new Date(b.report_date) - new Date(a.report_date);
|
|
case 'deadline':
|
|
if (!a.expected_completion_date && !b.expected_completion_date) return 0;
|
|
if (!a.expected_completion_date) return 1;
|
|
if (!b.expected_completion_date) return -1;
|
|
return new Date(a.expected_completion_date) - new Date(b.expected_completion_date);
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 유틸리티 함수들
|
|
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)');
|
|
}
|
|
|
|
// API 스크립트 동적 로드
|
|
const script = document.createElement('script');
|
|
script.src = '/static/js/api.js?v=' + Date.now();
|
|
script.onload = initializeDashboardApp;
|
|
document.body.appendChild(script);
|
|
</script>
|
|
</body>
|
|
</html>
|