feat: 현황판 UI를 관리함 진행 중 스타일로 개선
🎨 UI Redesign: - 기존 프로젝트별 그룹 카드 → 개별 이슈 카드로 변경 - 관리함 진행 중 페이지와 동일한 카드 형식 적용 - 읽기 전용으로 구현 (수정 기능 없음) 📋 Card Content: - 헤더: No., 긴급 표시, 등록일 - 기본 정보: 프로젝트, 부적합 내용, 카테고리 - 관리 정보: 해결방안, 담당부서, 담당자, 예상완료일, 원인부서 - 사진 정보: 업로드 사진 미리보기 (클릭 시 확대) 🔧 Features: - 내용이 없는 부분은 '-'로 표시 - 사진 모달 확대 기능 (ESC 키 지원) - 긴급도 표시 (예상완료일 3일 이내) - 반응형 그리드 레이아웃 (1/2/3 컬럼) 🎯 Layout: - 데스크톱: 3컬럼 그리드 - 태블릿: 2컬럼 그리드 - 모바일: 1컬럼 그리드 Expected Result: ✅ 관리함과 일관된 UI/UX ✅ 상세 정보 한눈에 확인 가능 ✅ 사진 확대 기능으로 편의성 향상 ✅ 읽기 전용으로 안전한 조회
This commit is contained in:
@@ -27,24 +27,31 @@
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.project-card {
|
||||
/* 이슈 카드 스타일 (관리함 진행 중 스타일) */
|
||||
.issue-card {
|
||||
transition: all 0.2s ease;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.project-card:hover {
|
||||
transform: translateX(5px);
|
||||
.issue-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-left-color: #3b82f6;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.issue-mini-card {
|
||||
.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-mini-card:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
.issue-card .bg-gray-50:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
@@ -359,7 +366,7 @@
|
||||
document.getElementById('activeProjects').textContent = activeProjectIds.size;
|
||||
}
|
||||
|
||||
// 프로젝트 카드 업데이트
|
||||
// 이슈 카드 업데이트 (관리함 진행 중 스타일)
|
||||
function updateProjectCards() {
|
||||
const container = document.getElementById('projectDashboard');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
@@ -372,101 +379,182 @@
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
// 프로젝트별 그룹화
|
||||
const projectGroups = {};
|
||||
filteredIssues.forEach(issue => {
|
||||
const projectId = issue.project_id || 'unassigned';
|
||||
if (!projectGroups[projectId]) {
|
||||
projectGroups[projectId] = [];
|
||||
}
|
||||
projectGroups[projectId].push(issue);
|
||||
});
|
||||
|
||||
// 프로젝트 카드 생성
|
||||
const projectCards = Object.keys(projectGroups).map(projectId => {
|
||||
const issues = projectGroups[projectId];
|
||||
const project = projects.find(p => p.id == projectId);
|
||||
const projectName = project ? project.project_name : '프로젝트 미지정';
|
||||
|
||||
return createProjectCard(projectName, issues);
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = projectCards;
|
||||
// 이슈 카드들을 생성 (관리함 진행 중 스타일)
|
||||
const issueCards = filteredIssues.map(issue => createIssueCard(issue)).join('');
|
||||
container.innerHTML = `<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">${issueCards}</div>`;
|
||||
}
|
||||
|
||||
// 프로젝트 카드 생성
|
||||
function createProjectCard(projectName, issues) {
|
||||
const urgentCount = issues.filter(issue => {
|
||||
// 이슈 카드 생성 (관리함 진행 중 스타일, 읽기 전용)
|
||||
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;
|
||||
}).length;
|
||||
return diffDays <= 3; // 3일 이내 또는 지연
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="project-card bg-white rounded-xl shadow-sm p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">${projectName}</h3>
|
||||
<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="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium">
|
||||
${issues.length}건
|
||||
</span>
|
||||
${urgentCount > 0 ? `<span class="bg-red-100 text-red-800 px-3 py-1 rounded-full text-sm font-medium">
|
||||
긴급 ${urgentCount}건
|
||||
</span>` : ''}
|
||||
<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>
|
||||
<span class="text-sm text-gray-500">${formatKSTDate(issue.report_date)}</span>
|
||||
</div>
|
||||
|
||||
<!-- 기본 정보 -->
|
||||
<div class="space-y-3 mb-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
|
||||
<div class="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded">${projectName}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">부적합 내용</label>
|
||||
<div class="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded min-h-[60px]">${issue.final_description || issue.description || '-'}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">카테고리</label>
|
||||
<div class="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded">${getCategoryText(issue.final_category || issue.category)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
${issues.map(issue => createIssueMiniCard(issue)).join('')}
|
||||
<!-- 관리 정보 -->
|
||||
<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>
|
||||
<div class="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded min-h-[40px]">${issue.solution || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">담당부서</label>
|
||||
<div class="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded">${getDepartmentText(issue.responsible_department)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">담당자</label>
|
||||
<div class="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded">${issue.responsible_person || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">예상완료일</label>
|
||||
<div class="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded">${formatKSTDate(issue.expected_completion_date)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">원인부서</label>
|
||||
<div class="text-sm text-gray-900 bg-gray-50 px-3 py-2 rounded">${getDepartmentText(issue.cause_department)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 사진 정보 -->
|
||||
${issue.photo_path || issue.photo_path2 ? `
|
||||
<div class="border-t pt-4 mt-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-16 h-16 bg-gray-100 rounded border flex items-center justify-center cursor-pointer hover:bg-gray-200" onclick="openPhotoModal('${issue.photo_path}')">
|
||||
<i class="fas fa-image text-gray-400"></i>
|
||||
</div>
|
||||
` : ''}
|
||||
${issue.photo_path2 ? `
|
||||
<div class="w-16 h-16 bg-gray-100 rounded border flex items-center justify-center cursor-pointer hover:bg-gray-200" onclick="openPhotoModal('${issue.photo_path2}')">
|
||||
<i class="fas fa-image text-gray-400"></i>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 이슈 미니 카드 생성
|
||||
function createIssueMiniCard(issue) {
|
||||
const isUrgent = issue.expected_completion_date &&
|
||||
(new Date(issue.expected_completion_date) - new Date()) / (1000 * 60 * 60 * 24) <= 3;
|
||||
// 사진 모달 열기
|
||||
function openPhotoModal(photoPath) {
|
||||
let modal = document.getElementById('photoModal');
|
||||
|
||||
const urgentClass = isUrgent ? 'border-red-200 bg-red-50' : 'border-gray-200 bg-white';
|
||||
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');
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="issue-mini-card ${urgentClass} border rounded-lg p-4 cursor-pointer"
|
||||
onclick="viewIssueDetail(${issue.id})">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-xs font-medium">
|
||||
No.${issue.project_sequence_no || '-'}
|
||||
</span>
|
||||
${isUrgent ? '<i class="fas fa-exclamation-triangle text-red-500"></i>' : ''}
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-800 mb-2 line-clamp-2">
|
||||
${issue.final_description || issue.description}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-gray-500">
|
||||
<span>${new Date(issue.report_date).toLocaleDateString('ko-KR')}</span>
|
||||
${issue.expected_completion_date ?
|
||||
`<span class="${isUrgent ? 'text-red-600 font-medium' : ''}">
|
||||
마감: ${new Date(issue.expected_completion_date).toLocaleDateString('ko-KR')}
|
||||
</span>` :
|
||||
'<span>마감일 미정</span>'
|
||||
}
|
||||
</div>
|
||||
|
||||
${issue.responsible_department ?
|
||||
`<div class="mt-2">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-gray-100 text-gray-800">
|
||||
${getDepartmentText(issue.responsible_department)}
|
||||
</span>
|
||||
</div>` : ''
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
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 filterByProject() {
|
||||
const projectId = document.getElementById('projectFilter').value;
|
||||
|
||||
Reference in New Issue
Block a user