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:
@@ -61,6 +61,77 @@
|
||||
font-size: 0.75rem;
|
||||
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-processing { background: #fef3c7; color: #92400e; }
|
||||
@@ -178,22 +249,16 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-800">부적합 관리</h2>
|
||||
<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()">
|
||||
<option value="priority">우선순위</option>
|
||||
<option value="newest">최신순</option>
|
||||
<option value="oldest">오래된순</option>
|
||||
<option value="status">상태순</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="issuesList" class="divide-y divide-gray-200">
|
||||
<!-- 부적합 목록이 여기에 동적으로 생성됩니다 -->
|
||||
<div id="issuesList" class="p-4">
|
||||
<!-- 날짜별 그룹화된 부적합 목록이 여기에 동적으로 생성됩니다 -->
|
||||
</div>
|
||||
|
||||
<!-- 빈 상태 -->
|
||||
@@ -386,16 +451,10 @@
|
||||
|
||||
filteredIssues.sort((a, b) => {
|
||||
switch (sortOrder) {
|
||||
case 'priority':
|
||||
const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 };
|
||||
return (priorityOrder[b.priority] || 1) - (priorityOrder[a.priority] || 1);
|
||||
case 'newest':
|
||||
return new Date(b.report_date) - new Date(a.report_date);
|
||||
case 'oldest':
|
||||
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:
|
||||
return new Date(b.report_date) - new Date(a.report_date);
|
||||
}
|
||||
@@ -414,52 +473,168 @@
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
container.innerHTML = filteredIssues.map(issue => {
|
||||
const project = projects.find(p => p.id === issue.project_id);
|
||||
const createdDate = new Date(issue.created_at).toLocaleDateString('ko-KR');
|
||||
const isSelected = selectedIssues.has(issue.id);
|
||||
// 날짜별로 그룹화
|
||||
const groupedByDate = {};
|
||||
filteredIssues.forEach(issue => {
|
||||
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 `
|
||||
<div class="issue-card p-6 status-${issue.status} ${isSelected ? 'bg-blue-50' : ''}"
|
||||
onclick="toggleIssueSelection(${issue.id})">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start space-x-3 flex-1">
|
||||
<input type="checkbox" ${isSelected ? 'checked' : ''}
|
||||
onchange="toggleIssueSelection(${issue.id})"
|
||||
onclick="event.stopPropagation()"
|
||||
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 class="date-group">
|
||||
<div class="date-header flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||
onclick="toggleDateGroup('${groupId}')">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fas fa-chevron-down transition-transform duration-200" id="icon-${groupId}"></i>
|
||||
<h3 class="font-semibold text-gray-800">${date}</h3>
|
||||
<span class="text-sm text-gray-500">(${issues.length}건)</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 ml-4">
|
||||
<button onclick="event.stopPropagation(); openStatusModal(${issue.id})"
|
||||
class="action-btn px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 transition-colors">
|
||||
<i class="fas fa-edit mr-1"></i>상태 변경
|
||||
</button>
|
||||
<button onclick="event.stopPropagation(); viewIssueDetail(${issue.id})"
|
||||
class="action-btn px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
|
||||
<i class="fas fa-eye mr-1"></i>상세
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="collapse-content" id="${groupId}">
|
||||
<div class="issue-table-container">
|
||||
<table class="issue-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No.</th>
|
||||
<th>프로젝트</th>
|
||||
<th>내용</th>
|
||||
<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>
|
||||
`;
|
||||
}).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