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:
Hyungi Ahn
2025-10-25 14:40:20 +09:00
parent d450ff3cbc
commit 20ebf530f9

View File

@@ -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);
}
// 통계 업데이트