feat: 관리함 이슈 표시 방식 완전 개편 - 테이블 형태 및 날짜별 그룹화
📊 Complete Issue Display Redesign: - 기존 카드 형태에서 테이블 형태로 완전 변경 - No.부터 완료 사진까지 모든 정보를 일렬로 표시 - 좌우 스크롤 가능한 테이블 구조 (min-width: 1200px) 📅 Date-based Grouping: - 날짜별로 이슈들을 그룹화하여 표시 - 각 날짜 그룹마다 접기/펼치기 기능 구현 - 날짜 헤더 클릭으로 해당 그룹 토글 가능 - 부드러운 애니메이션 효과 적용 🗂️ Comprehensive Data Display: - No. (프로젝트별 순번) - 프로젝트명 - 내용 (final_description 우선, 없으면 description) - 원인 (카테고리) - 해결방안 (solution) - 담당부서 (responsible_department) - 담당자 (responsible_person) - 조치예상일 (expected_completion_date) - 완료확인일 (actual_completion_date) - 확인자 (신고자 + 중복 신고자들) - 원인부서 (cause_department) - 의견 (management_comment) - 조치결과 (진행 중/완료됨) - 업로드 사진 (photo_path, photo_path2) - 완료 사진 (completion_photo_path) 🎨 Enhanced UI/UX: - 좌우 스크롤로 모든 정보 확인 가능 - 사진 클릭 시 확대 모달 표시 - 텍스트 오버플로우 시 툴팁으로 전체 내용 표시 - 상태별 색상 구분 (진행 중: 파란색, 완료됨: 초록색) - 호버 효과로 행 강조 🔧 Technical Implementation: - CSS 그리드 및 테이블 스타일링 - 반응형 스크롤 컨테이너 - 날짜 그룹 토글 애니메이션 - 사진 모달 팝업 기능 - 유틸리티 함수로 데이터 변환 🚀 User Experience: - 한 화면에서 모든 정보 확인 가능 - 날짜별 정리로 체계적인 관리 - 접기/펼치기로 필요한 정보만 표시 - 직관적인 테이블 형태로 데이터 비교 용이 Expected Result: ✅ No.부터 사진까지 모든 정보가 테이블 형태로 표시 ✅ 좌우 스크롤로 긴 데이터도 편리하게 확인 ✅ 날짜별 그룹화로 체계적인 관리 ✅ 접기/펼치기로 필요한 정보만 선택적 표시 ✅ 사진 클릭으로 확대 보기 가능
This commit is contained in:
@@ -62,6 +62,77 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 날짜 그룹 스타일 */
|
||||||
|
.date-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-header {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-header:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 좌우 스크롤 가능한 이슈 테이블 */
|
||||||
|
.issue-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-table {
|
||||||
|
min-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-table th,
|
||||||
|
.issue-table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-table th {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-table tbody tr:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-photo {
|
||||||
|
width: 60px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.issue-description {
|
||||||
|
max-width: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-content {
|
||||||
|
max-height: 1000px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-content.collapsed {
|
||||||
|
max-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-new { background: #dbeafe; color: #1e40af; }
|
.badge-new { background: #dbeafe; color: #1e40af; }
|
||||||
.badge-processing { background: #fef3c7; color: #92400e; }
|
.badge-processing { background: #fef3c7; color: #92400e; }
|
||||||
.badge-pending { background: #ede9fe; color: #7c3aed; }
|
.badge-pending { background: #ede9fe; color: #7c3aed; }
|
||||||
@@ -178,22 +249,16 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-semibold text-gray-800">부적합 관리</h2>
|
<h2 class="text-lg font-semibold text-gray-800">부적합 관리</h2>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" id="selectAll" onchange="toggleSelectAll()" class="mr-2">
|
|
||||||
<span class="text-sm text-gray-600">전체 선택</span>
|
|
||||||
</label>
|
|
||||||
<select id="sortOrder" class="text-sm border border-gray-300 rounded px-2 py-1" onchange="sortIssues()">
|
<select id="sortOrder" class="text-sm border border-gray-300 rounded px-2 py-1" onchange="sortIssues()">
|
||||||
<option value="priority">우선순위</option>
|
|
||||||
<option value="newest">최신순</option>
|
<option value="newest">최신순</option>
|
||||||
<option value="oldest">오래된순</option>
|
<option value="oldest">오래된순</option>
|
||||||
<option value="status">상태순</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="issuesList" class="divide-y divide-gray-200">
|
<div id="issuesList" class="p-4">
|
||||||
<!-- 부적합 목록이 여기에 동적으로 생성됩니다 -->
|
<!-- 날짜별 그룹화된 부적합 목록이 여기에 동적으로 생성됩니다 -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 빈 상태 -->
|
<!-- 빈 상태 -->
|
||||||
@@ -386,16 +451,10 @@
|
|||||||
|
|
||||||
filteredIssues.sort((a, b) => {
|
filteredIssues.sort((a, b) => {
|
||||||
switch (sortOrder) {
|
switch (sortOrder) {
|
||||||
case 'priority':
|
|
||||||
const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 };
|
|
||||||
return (priorityOrder[b.priority] || 1) - (priorityOrder[a.priority] || 1);
|
|
||||||
case 'newest':
|
case 'newest':
|
||||||
return new Date(b.report_date) - new Date(a.report_date);
|
return new Date(b.report_date) - new Date(a.report_date);
|
||||||
case 'oldest':
|
case 'oldest':
|
||||||
return new Date(a.report_date) - new Date(b.report_date);
|
return new Date(a.report_date) - new Date(b.report_date);
|
||||||
case 'status':
|
|
||||||
const statusOrder = { 'new': 4, 'processing': 3, 'pending': 2, 'completed': 1 };
|
|
||||||
return (statusOrder[b.status] || 0) - (statusOrder[a.status] || 0);
|
|
||||||
default:
|
default:
|
||||||
return new Date(b.report_date) - new Date(a.report_date);
|
return new Date(b.report_date) - new Date(a.report_date);
|
||||||
}
|
}
|
||||||
@@ -414,52 +473,168 @@
|
|||||||
|
|
||||||
emptyState.classList.add('hidden');
|
emptyState.classList.add('hidden');
|
||||||
|
|
||||||
container.innerHTML = filteredIssues.map(issue => {
|
// 날짜별로 그룹화
|
||||||
const project = projects.find(p => p.id === issue.project_id);
|
const groupedByDate = {};
|
||||||
const createdDate = new Date(issue.created_at).toLocaleDateString('ko-KR');
|
filteredIssues.forEach(issue => {
|
||||||
const isSelected = selectedIssues.has(issue.id);
|
const date = new Date(issue.report_date).toLocaleDateString('ko-KR');
|
||||||
|
if (!groupedByDate[date]) {
|
||||||
|
groupedByDate[date] = [];
|
||||||
|
}
|
||||||
|
groupedByDate[date].push(issue);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 날짜별 그룹을 HTML로 생성
|
||||||
|
const dateGroups = Object.keys(groupedByDate).map(date => {
|
||||||
|
const issues = groupedByDate[date];
|
||||||
|
const groupId = `group-${date.replace(/\./g, '-')}`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="issue-card p-6 status-${issue.status} ${isSelected ? 'bg-blue-50' : ''}"
|
<div class="date-group">
|
||||||
onclick="toggleIssueSelection(${issue.id})">
|
<div class="date-header flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||||
<div class="flex items-start justify-between">
|
onclick="toggleDateGroup('${groupId}')">
|
||||||
<div class="flex items-start space-x-3 flex-1">
|
<div class="flex items-center space-x-3">
|
||||||
<input type="checkbox" ${isSelected ? 'checked' : ''}
|
<i class="fas fa-chevron-down transition-transform duration-200" id="icon-${groupId}"></i>
|
||||||
onchange="toggleIssueSelection(${issue.id})"
|
<h3 class="font-semibold text-gray-800">${date}</h3>
|
||||||
onclick="event.stopPropagation()"
|
<span class="text-sm text-gray-500">(${issues.length}건)</span>
|
||||||
class="mt-1">
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-center space-x-3 mb-2">
|
|
||||||
<span class="badge badge-${getStatusBadgeClass(issue.status)}">${getStatusText(issue.status)}</span>
|
|
||||||
${getPriorityBadge(issue.priority)}
|
|
||||||
${project ? `<span class="text-sm text-gray-500">${project.project_name}</span>` : ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-lg font-medium text-gray-900 mb-2">${issue.description}</h3>
|
|
||||||
|
|
||||||
<div class="flex items-center text-sm text-gray-500 space-x-4">
|
|
||||||
<span><i class="fas fa-user mr-1"></i>${issue.reporter?.username || '알 수 없음'}</span>
|
|
||||||
<span><i class="fas fa-calendar mr-1"></i>${createdDate}</span>
|
|
||||||
${issue.category ? `<span><i class="fas fa-tag mr-1"></i>${getCategoryText(issue.category)}</span>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2 ml-4">
|
<div class="collapse-content" id="${groupId}">
|
||||||
<button onclick="event.stopPropagation(); openStatusModal(${issue.id})"
|
<div class="issue-table-container">
|
||||||
class="action-btn px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition-colors">
|
<table class="issue-table">
|
||||||
<i class="fas fa-edit mr-1"></i>상태 변경
|
<thead>
|
||||||
</button>
|
<tr>
|
||||||
<button onclick="event.stopPropagation(); viewIssueDetail(${issue.id})"
|
<th>No.</th>
|
||||||
class="action-btn px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
|
<th>프로젝트</th>
|
||||||
<i class="fas fa-eye mr-1"></i>상세
|
<th>내용</th>
|
||||||
</button>
|
<th>원인</th>
|
||||||
|
<th>해결방안</th>
|
||||||
|
<th>담당부서</th>
|
||||||
|
<th>담당자</th>
|
||||||
|
<th>조치예상일</th>
|
||||||
|
<th>완료확인일</th>
|
||||||
|
<th>확인자</th>
|
||||||
|
<th>원인부서</th>
|
||||||
|
<th>의견</th>
|
||||||
|
<th>조치결과</th>
|
||||||
|
<th>업로드 사진</th>
|
||||||
|
<th>완료 사진</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${issues.map(issue => createIssueRow(issue)).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
container.innerHTML = dateGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이슈 행 생성 함수
|
||||||
|
function createIssueRow(issue) {
|
||||||
|
const project = projects.find(p => p.id === issue.project_id);
|
||||||
|
const statusText = issue.review_status === 'in_progress' ? '진행 중' : '완료됨';
|
||||||
|
const statusClass = issue.review_status === 'in_progress' ? 'text-blue-600' : 'text-green-600';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td class="font-medium">${issue.project_sequence_no || '-'}</td>
|
||||||
|
<td>${project ? project.project_name : '-'}</td>
|
||||||
|
<td class="issue-description" title="${issue.final_description || issue.description}">
|
||||||
|
${issue.final_description || issue.description}
|
||||||
|
</td>
|
||||||
|
<td>${getCategoryText(issue.final_category || issue.category)}</td>
|
||||||
|
<td class="issue-description" title="${issue.solution || '-'}">
|
||||||
|
${issue.solution || '-'}
|
||||||
|
</td>
|
||||||
|
<td>${getDepartmentText(issue.responsible_department) || '-'}</td>
|
||||||
|
<td>${issue.responsible_person || '-'}</td>
|
||||||
|
<td>${issue.expected_completion_date ? new Date(issue.expected_completion_date).toLocaleDateString('ko-KR') : '-'}</td>
|
||||||
|
<td>${issue.actual_completion_date ? new Date(issue.actual_completion_date).toLocaleDateString('ko-KR') : '-'}</td>
|
||||||
|
<td>${getReporterNames(issue)}</td>
|
||||||
|
<td>${getDepartmentText(issue.cause_department) || '-'}</td>
|
||||||
|
<td class="issue-description" title="${issue.management_comment || '-'}">
|
||||||
|
${issue.management_comment || '-'}
|
||||||
|
</td>
|
||||||
|
<td class="${statusClass} font-medium">${statusText}</td>
|
||||||
|
<td>
|
||||||
|
${issue.photo_path ? `<img src="${issue.photo_path}" class="issue-photo" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진">` : '-'}
|
||||||
|
${issue.photo_path2 ? `<br><img src="${issue.photo_path2}" class="issue-photo mt-1" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : ''}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${issue.completion_photo_path ? `<img src="${issue.completion_photo_path}" class="issue-photo" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">` : '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 날짜 그룹 토글 함수
|
||||||
|
function toggleDateGroup(groupId) {
|
||||||
|
const content = document.getElementById(groupId);
|
||||||
|
const icon = document.getElementById(`icon-${groupId}`);
|
||||||
|
|
||||||
|
if (content.classList.contains('collapsed')) {
|
||||||
|
content.classList.remove('collapsed');
|
||||||
|
icon.style.transform = 'rotate(0deg)';
|
||||||
|
} else {
|
||||||
|
content.classList.add('collapsed');
|
||||||
|
icon.style.transform = 'rotate(-90deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 유틸리티 함수들
|
||||||
|
function getDepartmentText(department) {
|
||||||
|
const departments = {
|
||||||
|
'production': '생산',
|
||||||
|
'quality': '품질',
|
||||||
|
'purchasing': '구매',
|
||||||
|
'design': '설계',
|
||||||
|
'sales': '영업'
|
||||||
|
};
|
||||||
|
return departments[department] || department;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReporterNames(issue) {
|
||||||
|
let names = [issue.reporter?.full_name || issue.reporter?.username || '알 수 없음'];
|
||||||
|
|
||||||
|
if (issue.duplicate_reporters && issue.duplicate_reporters.length > 0) {
|
||||||
|
const duplicateNames = issue.duplicate_reporters.map(r => r.full_name || r.username);
|
||||||
|
names = names.concat(duplicateNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
return names.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryText(category) {
|
||||||
|
const categories = {
|
||||||
|
'quality': '품질',
|
||||||
|
'safety': '안전',
|
||||||
|
'environment': '환경',
|
||||||
|
'process': '공정',
|
||||||
|
'equipment': '장비',
|
||||||
|
'material': '자재',
|
||||||
|
'etc': '기타'
|
||||||
|
};
|
||||||
|
return categories[category] || category;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사진 모달 함수
|
||||||
|
function openPhotoModal(photoPath) {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50';
|
||||||
|
modal.onclick = () => modal.remove();
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="max-w-4xl max-h-4xl p-4">
|
||||||
|
<img src="${photoPath}" class="max-w-full max-h-full object-contain rounded-lg" alt="확대된 사진">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 통계 업데이트
|
// 통계 업데이트
|
||||||
|
|||||||
Reference in New Issue
Block a user