🎯 부적합 정보 구조화 개선: 📝 수신함 검토 모달 개선: - '설명' → '부적합명' + '상세 내용'으로 분리 - 부적합명: 간단한 제목 (필수 입력) - 상세 내용: 자세한 설명 (선택 입력) - 저장 시 첫 번째 줄에 제목, 나머지는 상세 내용으로 결합 🏢 관리함 진행중 페이지 개선: - No. 옆에 프로젝트명 표시 (작은 글씨) - 부적합명을 카드 헤더에 큰 제목으로 표시 - '부적합 내용' → '상세 내용'으로 변경 - getIssueTitle(), getIssueDetail() 헬퍼 함수 추가 📊 현황판 페이지 개선: - No. 옆에 프로젝트명과 카테고리 태그 표시 - 부적합명을 카드 헤더에 제목으로 표시 - '부적합 내용' → '상세 내용'으로 변경 - 동일한 헬퍼 함수로 일관성 유지 💡 핵심 개선사항: - 정보 계층 구조 명확화 (제목 vs 상세) - 시각적 가독성 향상 (헤더에 중요 정보 집중) - 일관된 표시 방식 (수신함 → 관리함 → 현황판) - 기존 데이터 호환성 유지 🔧 기술적 구현: - 첫 번째 줄을 제목으로 추출 - 두 번째 줄부터를 상세 내용으로 분리 - 기존 description 필드 활용 (DB 변경 없음) - 폴백 처리로 안정성 확보 Expected Result: ✅ 부적합 정보의 체계적 관리 ✅ 한눈에 파악 가능한 제목 표시 ✅ 상세 내용과 요약 정보 분리 ✅ 전체 워크플로우 일관성 향상
1605 lines
75 KiB
HTML
1605 lines
75 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>
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Font Awesome -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
|
|
<!-- 모바일 캘린더 스타일 -->
|
|
<link rel="stylesheet" href="/static/css/mobile-calendar.css">
|
|
|
|
<!-- Custom Styles -->
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.issue-card {
|
|
transition: all 0.2s ease;
|
|
border-left: 4px solid transparent;
|
|
}
|
|
|
|
.issue-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.priority-high { border-left-color: #ef4444; }
|
|
.priority-medium { border-left-color: #f59e0b; }
|
|
.priority-low { border-left-color: #10b981; }
|
|
|
|
.status-new { border-left-color: #3b82f6; }
|
|
.status-processing { border-left-color: #f59e0b; }
|
|
.status-pending { border-left-color: #8b5cf6; }
|
|
.status-completed { border-left-color: #10b981; }
|
|
|
|
.action-btn {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.modal {
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 9999px;
|
|
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: 2000px; /* 더 넓은 최소 너비 설정 */
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.issue-table th,
|
|
.issue-table td {
|
|
padding: 0.75rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.issue-table th {
|
|
background-color: #f9fafb;
|
|
font-weight: 600;
|
|
color: #374151;
|
|
font-size: 0.875rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.issue-table tbody tr:hover {
|
|
background-color: #f9fafb;
|
|
}
|
|
|
|
/* 컬럼별 너비 조정 */
|
|
.col-no { min-width: 60px; }
|
|
.col-project { min-width: 120px; }
|
|
.col-content { min-width: 250px; max-width: 300px; }
|
|
.col-cause { min-width: 100px; }
|
|
.col-solution { min-width: 200px; max-width: 250px; }
|
|
.col-department { min-width: 100px; }
|
|
.col-person { min-width: 120px; }
|
|
.col-date { min-width: 120px; }
|
|
.col-confirmer { min-width: 120px; }
|
|
.col-comment { min-width: 200px; max-width: 250px; }
|
|
.col-status { min-width: 100px; }
|
|
.col-photos { min-width: 150px; }
|
|
.col-completion { min-width: 80px; }
|
|
.col-actions { min-width: 120px; }
|
|
|
|
.issue-photo {
|
|
width: 60px;
|
|
height: 40px;
|
|
object-fit: cover;
|
|
border-radius: 0.375rem;
|
|
cursor: pointer;
|
|
margin: 2px;
|
|
}
|
|
|
|
.photo-container {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
}
|
|
|
|
/* 편집 가능한 필드 스타일 */
|
|
.editable-field {
|
|
min-width: 100%;
|
|
padding: 4px 8px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 4px;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.editable-field:focus {
|
|
outline: none;
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 1px #3b82f6;
|
|
}
|
|
|
|
.text-wrap {
|
|
white-space: normal;
|
|
word-wrap: break-word;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 4px 8px;
|
|
font-size: 0.75rem;
|
|
border-radius: 4px;
|
|
margin: 2px;
|
|
white-space: nowrap;
|
|
min-width: fit-content;
|
|
}
|
|
|
|
.collapse-content {
|
|
max-height: 1000px;
|
|
overflow: hidden;
|
|
transition: max-height 0.3s ease-out;
|
|
}
|
|
|
|
.collapse-content.collapsed {
|
|
max-height: 0;
|
|
}
|
|
|
|
/* 진행 중 카드 스타일 */
|
|
.issue-card {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.issue-card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.issue-card label {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.issue-card input:focus,
|
|
.issue-card select:focus,
|
|
.issue-card textarea:focus {
|
|
transform: scale(1.01);
|
|
transition: transform 0.1s ease;
|
|
}
|
|
|
|
.issue-card .bg-gray-50 {
|
|
border-left: 4px solid #e5e7eb;
|
|
}
|
|
|
|
/* 카드 내 아이콘 스타일 */
|
|
.issue-card i {
|
|
width: 16px;
|
|
text-align: center;
|
|
}
|
|
|
|
.badge-new { background: #dbeafe; color: #1e40af; }
|
|
.badge-processing { background: #fef3c7; color: #92400e; }
|
|
.badge-pending { background: #ede9fe; color: #7c3aed; }
|
|
.badge-completed { background: #d1fae5; color: #065f46; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
|
|
|
|
<!-- Main Content -->
|
|
<main class="container mx-auto px-4 py-8" style="padding-top: 80px;">
|
|
<!-- 페이지 헤더 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
|
|
<i class="fas fa-cog text-green-500 mr-3"></i>
|
|
관리함
|
|
</h1>
|
|
<p class="text-gray-600 mt-1">부적합 사항을 처리하고 상태를 관리하세요</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- 프로젝트 필터 및 상태 탭 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
|
<div class="space-y-4">
|
|
<!-- 프로젝트 선택 -->
|
|
<div class="max-w-md">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">📁 프로젝트</label>
|
|
<select id="projectFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500" onchange="filterIssues()">
|
|
<option value="">전체 프로젝트</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 상태 탭 및 추가 정보 버튼 -->
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex space-x-1 bg-gray-100 p-1 rounded-lg max-w-md">
|
|
<button id="inProgressTab"
|
|
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-blue-500 text-white"
|
|
onclick="switchTab('in_progress')">
|
|
<i class="fas fa-cog mr-2"></i>진행 중
|
|
</button>
|
|
<button id="completedTab"
|
|
class="flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900"
|
|
onclick="switchTab('completed')">
|
|
<i class="fas fa-check-circle mr-2"></i>완료됨
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 추가 정보 입력 버튼 (진행 중 탭에서만 표시) -->
|
|
<button id="additionalInfoBtn"
|
|
class="px-4 py-2 bg-orange-500 text-white text-sm font-medium rounded-lg hover:bg-orange-600 transition-colors duration-200 shadow-sm"
|
|
onclick="openAdditionalInfoModal()"
|
|
style="display: none;">
|
|
<i class="fas fa-plus-circle mr-2"></i>추가 정보 입력
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 프로젝트별 통계 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="bg-gray-50 p-4 rounded-lg">
|
|
<div class="flex items-center">
|
|
<i class="fas fa-chart-bar text-gray-500 text-xl mr-3"></i>
|
|
<div>
|
|
<p class="text-sm text-gray-600">총 부적합</p>
|
|
<p class="text-2xl font-bold text-gray-700" id="totalCount">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-blue-50 p-4 rounded-lg">
|
|
<div class="flex items-center">
|
|
<i class="fas fa-cog text-blue-500 text-xl mr-3"></i>
|
|
<div>
|
|
<p class="text-sm text-blue-600">진행 중</p>
|
|
<p class="text-2xl font-bold text-blue-700" id="inProgressCount">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-green-50 p-4 rounded-lg">
|
|
<div class="flex items-center">
|
|
<i class="fas fa-check-circle text-green-500 text-xl mr-3"></i>
|
|
<div>
|
|
<p class="text-sm text-green-600">완료됨</p>
|
|
<p class="text-2xl font-bold text-green-700" id="completedCount">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 부적합 관리 목록 -->
|
|
<div class="bg-white rounded-xl shadow-sm">
|
|
<div class="p-6 border-b border-gray-200">
|
|
<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">
|
|
<select id="sortOrder" class="text-sm border border-gray-300 rounded px-2 py-1" onchange="sortIssues()">
|
|
<option value="newest">최신순</option>
|
|
<option value="oldest">오래된순</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="issuesList" class="p-4">
|
|
<!-- 날짜별 그룹화된 부적합 목록이 여기에 동적으로 생성됩니다 -->
|
|
</div>
|
|
|
|
<!-- 빈 상태 -->
|
|
<div id="emptyState" class="hidden p-12 text-center">
|
|
<i class="fas fa-cog 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>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- 완료된 이슈 상세보기 모달 -->
|
|
<div id="issueDetailModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="bg-white rounded-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
|
<div class="p-6">
|
|
<!-- 모달 헤더 -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-semibold text-gray-900" id="modalTitle">부적합 상세 정보</h2>
|
|
<button onclick="closeIssueDetailModal()" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times text-xl"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 모달 내용 -->
|
|
<div id="modalContent" class="space-y-6">
|
|
<!-- 동적으로 생성될 내용 -->
|
|
</div>
|
|
|
|
<!-- 모달 푸터 -->
|
|
<div class="flex justify-end space-x-3 mt-6 pt-6 border-t">
|
|
<button onclick="closeIssueDetailModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
|
취소
|
|
</button>
|
|
<button onclick="saveModalChanges()" class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 상태 변경 모달 -->
|
|
<div id="statusModal" class="fixed inset-0 bg-black bg-opacity-50 modal hidden z-50">
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-900">상태 변경</h3>
|
|
<button onclick="closeStatusModal()" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">새 상태</label>
|
|
<select id="newStatus" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
|
|
<option value="processing">처리 중</option>
|
|
<option value="pending">대기 중</option>
|
|
<option value="completed">완료</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">처리 메모</label>
|
|
<textarea id="statusNote" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
|
placeholder="상태 변경 사유나 처리 내용을 입력하세요..."></textarea>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3">
|
|
<button onclick="closeStatusModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
|
취소
|
|
</button>
|
|
<button onclick="updateStatus()" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600">
|
|
변경
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts -->
|
|
<script src="/static/js/date-utils.js?v=20250917"></script>
|
|
<script src="/static/js/core/permissions.js?v=20251025"></script>
|
|
<script src="/static/js/components/common-header.js?v=20251025"></script>
|
|
<script src="/static/js/core/page-manager.js?v=20251025"></script>
|
|
<script>
|
|
let currentUser = null;
|
|
let issues = [];
|
|
let projects = [];
|
|
let filteredIssues = [];
|
|
let currentIssueId = null;
|
|
let currentTab = 'in_progress'; // 기본값: 진행 중
|
|
|
|
// API 로드 후 초기화 함수
|
|
async function initializeManagement() {
|
|
const token = localStorage.getItem('access_token');
|
|
if (!token) {
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const user = await AuthAPI.getCurrentUser();
|
|
currentUser = user;
|
|
localStorage.setItem('currentUser', JSON.stringify(user));
|
|
|
|
// 공통 헤더 초기화
|
|
await window.commonHeader.init(user, 'issues_management');
|
|
|
|
// 페이지 접근 권한 체크
|
|
setTimeout(() => {
|
|
if (!canAccessPage('issues_management')) {
|
|
alert('관리함 페이지에 접근할 권한이 없습니다.');
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
}, 500);
|
|
|
|
// 데이터 로드
|
|
await loadProjects();
|
|
await loadIssues();
|
|
|
|
} catch (error) {
|
|
console.error('인증 실패:', error);
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('currentUser');
|
|
window.location.href = '/index.html';
|
|
}
|
|
}
|
|
|
|
// 프로젝트 로드
|
|
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();
|
|
}
|
|
} catch (error) {
|
|
console.error('프로젝트 로드 실패:', error);
|
|
}
|
|
}
|
|
|
|
// 부적합 목록 로드 (관리자는 모든 부적합 조회)
|
|
async function loadIssues() {
|
|
try {
|
|
let endpoint = '/api/issues/admin/all';
|
|
|
|
const response = await fetch(endpoint, {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const allIssues = await response.json();
|
|
// 관리함에서는 진행 중(in_progress)과 완료됨(completed) 상태만 표시
|
|
let filteredIssues = allIssues.filter(issue =>
|
|
issue.review_status === 'in_progress' || issue.review_status === 'completed'
|
|
);
|
|
|
|
// 수신함에서 넘어온 순서대로 No. 재할당 (reviewed_at 기준)
|
|
filteredIssues.sort((a, b) => new Date(a.reviewed_at) - new Date(b.reviewed_at));
|
|
|
|
// 프로젝트별로 그룹화하여 No. 재할당
|
|
const projectGroups = {};
|
|
filteredIssues.forEach(issue => {
|
|
if (!projectGroups[issue.project_id]) {
|
|
projectGroups[issue.project_id] = [];
|
|
}
|
|
projectGroups[issue.project_id].push(issue);
|
|
});
|
|
|
|
// 각 프로젝트별로 순번 재할당
|
|
Object.keys(projectGroups).forEach(projectId => {
|
|
projectGroups[projectId].forEach((issue, index) => {
|
|
issue.project_sequence_no = index + 1;
|
|
});
|
|
});
|
|
|
|
issues = filteredIssues;
|
|
filterIssues();
|
|
} else {
|
|
throw new Error('부적합 목록을 불러올 수 없습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('부적합 로드 실패:', error);
|
|
alert('부적합 목록을 불러오는데 실패했습니다.');
|
|
}
|
|
}
|
|
|
|
// 탭 전환 함수
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
|
|
// 탭 버튼 스타일 업데이트
|
|
const inProgressTab = document.getElementById('inProgressTab');
|
|
const completedTab = document.getElementById('completedTab');
|
|
const additionalInfoBtn = document.getElementById('additionalInfoBtn');
|
|
|
|
if (tab === 'in_progress') {
|
|
inProgressTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-blue-500 text-white';
|
|
completedTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900';
|
|
// 진행 중 탭에서만 추가 정보 버튼 표시
|
|
additionalInfoBtn.style.display = 'block';
|
|
} else {
|
|
inProgressTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 text-gray-600 hover:text-gray-900';
|
|
completedTab.className = 'flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors duration-200 bg-green-500 text-white';
|
|
// 완료됨 탭에서는 추가 정보 버튼 숨김
|
|
additionalInfoBtn.style.display = 'none';
|
|
}
|
|
|
|
filterIssues(); // 이미 updateStatistics()가 포함됨
|
|
}
|
|
|
|
// 통계 업데이트 함수
|
|
function updateStatistics() {
|
|
const projectFilter = document.getElementById('projectFilter').value;
|
|
|
|
// 선택된 프로젝트에 따른 이슈 필터링
|
|
const projectIssues = projectFilter
|
|
? issues.filter(issue => issue.project_id == projectFilter)
|
|
: issues;
|
|
|
|
// 상태별 카운트
|
|
const totalCount = projectIssues.length;
|
|
const inProgressCount = projectIssues.filter(issue => issue.review_status === 'in_progress').length;
|
|
const completedCount = projectIssues.filter(issue => issue.review_status === 'completed').length;
|
|
|
|
// 통계 업데이트
|
|
document.getElementById('totalCount').textContent = totalCount;
|
|
document.getElementById('inProgressCount').textContent = inProgressCount;
|
|
document.getElementById('completedCount').textContent = completedCount;
|
|
}
|
|
|
|
// 필터링 및 표시 함수들
|
|
function filterIssues() {
|
|
const projectFilter = document.getElementById('projectFilter').value;
|
|
|
|
filteredIssues = issues.filter(issue => {
|
|
// 현재 탭에 따른 상태 필터링
|
|
if (issue.review_status !== currentTab) return false;
|
|
|
|
// 프로젝트 필터링
|
|
if (projectFilter && issue.project_id != projectFilter) return false;
|
|
|
|
return true;
|
|
});
|
|
|
|
sortIssues();
|
|
displayIssues();
|
|
updateStatistics(); // 통계 업데이트 추가
|
|
}
|
|
|
|
function sortIssues() {
|
|
const sortOrder = document.getElementById('sortOrder').value;
|
|
|
|
filteredIssues.sort((a, b) => {
|
|
switch (sortOrder) {
|
|
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);
|
|
default:
|
|
return new Date(b.report_date) - new Date(a.report_date);
|
|
}
|
|
});
|
|
}
|
|
|
|
function displayIssues() {
|
|
const container = document.getElementById('issuesList');
|
|
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 => {
|
|
let date;
|
|
if (currentTab === 'in_progress') {
|
|
// 진행 중: 업로드한 날짜 기준
|
|
date = new Date(issue.report_date).toLocaleDateString('ko-KR');
|
|
} else {
|
|
// 완료됨: 완료된 날짜 기준 (없으면 업로드 날짜)
|
|
const completionDate = issue.actual_completion_date || issue.report_date;
|
|
date = new Date(completionDate).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="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>
|
|
<span class="text-xs px-2 py-1 rounded-full ${currentTab === 'in_progress' ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600'}">
|
|
${currentTab === 'in_progress' ? '업로드일' : '완료일'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="collapse-content" id="${groupId}">
|
|
${currentTab === 'in_progress' ?
|
|
// 진행 중: 카드 형식
|
|
`<div class="space-y-4 mt-4">
|
|
${issues.map(issue => createIssueRow(issue)).join('')}
|
|
</div>` :
|
|
// 완료됨: 테이블 형식
|
|
`<div class="issue-table-container">
|
|
<table class="issue-table">
|
|
<thead>
|
|
<tr>
|
|
${createTableHeader()}
|
|
</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 isInProgress = issue.review_status === 'in_progress';
|
|
const isCompleted = issue.review_status === 'completed';
|
|
|
|
if (isInProgress) {
|
|
// 진행 중 - 편집 가능한 형태
|
|
return createInProgressRow(issue, project);
|
|
} else {
|
|
// 완료됨 - 입력 여부 표시 + 클릭으로 상세보기
|
|
return createCompletedRow(issue, project);
|
|
}
|
|
}
|
|
|
|
// 부적합명 추출 (첫 번째 줄)
|
|
function getIssueTitle(issue) {
|
|
const description = issue.final_description || issue.description || '';
|
|
const lines = description.split('\n');
|
|
return lines[0] || '부적합명 없음';
|
|
}
|
|
|
|
// 상세 내용 추출 (두 번째 줄부터)
|
|
function getIssueDetail(issue) {
|
|
const description = issue.final_description || issue.description || '';
|
|
const lines = description.split('\n');
|
|
return lines.slice(1).join('\n') || '상세 내용 없음';
|
|
}
|
|
|
|
// 진행 중 카드 생성
|
|
function createInProgressRow(issue, project) {
|
|
return `
|
|
<div class="issue-card bg-white border border-gray-200 rounded-xl p-6 mb-4 shadow-sm hover:shadow-md transition-shadow" data-issue-id="${issue.id}">
|
|
<!-- 카드 헤더 -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex flex-col space-y-1">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-xl font-bold bg-gradient-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent">No.${issue.project_sequence_no || '-'}</span>
|
|
<div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse"></div>
|
|
</div>
|
|
<span class="text-sm text-gray-600">${project ? project.project_name : '프로젝트 미지정'}</span>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-gray-900">${getIssueTitle(issue)}</h3>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button onclick="saveIssueChanges(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
|
<i class="fas fa-save mr-1"></i>저장
|
|
</button>
|
|
<button onclick="completeIssue(${issue.id})" class="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
|
|
<i class="fas fa-check mr-1"></i>완료처리
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 카드 내용 -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- 왼쪽: 기본 정보 -->
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">상세 내용</label>
|
|
<div class="p-3 bg-gray-50 rounded-lg text-gray-800 min-h-[80px]">
|
|
${getIssueDetail(issue)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">원인 분류</label>
|
|
<div class="p-3 bg-gray-50 rounded-lg text-gray-800">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
|
${getCategoryText(issue.final_category || issue.category)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">업로드 사진</label>
|
|
<div class="flex gap-2">
|
|
${issue.photo_path ? `<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진 1">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs border-2 border-dashed border-gray-300">사진 없음</div>'}
|
|
${issue.photo_path2 ? `<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs border-2 border-dashed border-gray-300">사진 없음</div>'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 오른쪽: 편집 가능한 정보 -->
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
<i class="fas fa-lightbulb text-yellow-500 mr-1"></i>해결방안
|
|
</label>
|
|
<textarea id="solution_${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none" placeholder="해결 방안을 입력하세요...">${issue.solution || ''}</textarea>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
<i class="fas fa-building text-blue-500 mr-1"></i>담당부서
|
|
</label>
|
|
<select id="responsible_department_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
${getDepartmentOptions().map(opt =>
|
|
`<option value="${opt.value}" ${opt.value === (issue.responsible_department || '') ? 'selected' : ''}>${opt.text}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
<i class="fas fa-user text-purple-500 mr-1"></i>담당자
|
|
</label>
|
|
<input type="text" id="responsible_person_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="담당자 이름" value="${issue.responsible_person || ''}">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
<i class="fas fa-calendar-alt text-red-500 mr-1"></i>조치 예상일
|
|
</label>
|
|
<input type="date" id="expected_completion_date_${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="${issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : ''}">
|
|
</div>
|
|
|
|
<!-- 진행 상태 표시 -->
|
|
<div class="mt-4 p-3 bg-blue-50 rounded-lg">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-blue-700 font-medium">
|
|
<i class="fas fa-clock mr-1"></i>진행 중
|
|
</span>
|
|
<span class="text-xs text-blue-600">
|
|
신고일: ${new Date(issue.report_date).toLocaleDateString('ko-KR')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 완료됨 행 생성 (입력 여부 표시)
|
|
function createCompletedRow(issue, project) {
|
|
return `
|
|
<tr data-issue-id="${issue.id}" class="cursor-pointer hover:bg-blue-50" onclick="openIssueDetailModal(${issue.id})">
|
|
<td class="col-no font-medium">
|
|
<span class="inline-flex items-center space-x-1">
|
|
<span class="text-lg font-bold bg-gradient-to-r from-green-600 to-green-800 bg-clip-text text-transparent">No.${issue.project_sequence_no || '-'}</span>
|
|
<div class="w-1.5 h-1.5 bg-green-500 rounded-full"></div>
|
|
</span>
|
|
</td>
|
|
<td class="col-project">${project ? project.project_name : '-'}</td>
|
|
<td class="col-content">${getStatusIcon(issue.final_description || issue.description)}</td>
|
|
<td class="col-cause">${getStatusIcon(getCategoryText(issue.final_category || issue.category))}</td>
|
|
<td class="col-solution">${getStatusIcon(issue.solution)}</td>
|
|
<td class="col-department">${getStatusIcon(issue.responsible_department)}</td>
|
|
<td class="col-person">${getStatusIcon(issue.responsible_person)}</td>
|
|
<td class="col-date">${getStatusIcon(issue.expected_completion_date)}</td>
|
|
<td class="col-confirmer">${getReporterNames(issue)}</td>
|
|
<td class="col-department">${getStatusIcon(issue.cause_department)}</td>
|
|
<td class="col-comment">${getStatusIcon(issue.management_comment)}</td>
|
|
<td class="col-photos">${getPhotoStatusIcon(issue.photo_path, issue.photo_path2)}</td>
|
|
<td class="col-completion">${getStatusIcon(issue.completion_photo_path)}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
// 입력 여부 아이콘 생성
|
|
function getStatusIcon(value) {
|
|
if (value && value.toString().trim() !== '') {
|
|
return '<span class="text-green-500 text-lg">✅</span>';
|
|
} else {
|
|
return '<span class="text-gray-400 text-lg">❌</span>';
|
|
}
|
|
}
|
|
|
|
// 사진 상태 아이콘 생성
|
|
function getPhotoStatusIcon(photo1, photo2) {
|
|
const count = (photo1 ? 1 : 0) + (photo2 ? 1 : 0);
|
|
if (count > 0) {
|
|
return `<span class="text-green-500 text-lg">✅</span><span class="text-xs ml-1">${count}장</span>`;
|
|
} else {
|
|
return '<span class="text-gray-400 text-lg">❌</span>';
|
|
}
|
|
}
|
|
|
|
// 테이블 헤더 생성 함수
|
|
function createTableHeader() {
|
|
if (currentTab === 'in_progress') {
|
|
// 진행 중 탭 헤더
|
|
return `
|
|
<th class="col-no">
|
|
<span class="inline-flex items-center space-x-1">
|
|
<span class="font-bold bg-gradient-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent">No.</span>
|
|
<div class="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
|
|
</span>
|
|
</th>
|
|
<th class="col-project">프로젝트</th>
|
|
<th class="col-content">내용</th>
|
|
<th class="col-cause">원인</th>
|
|
<th class="col-solution">해결방안</th>
|
|
<th class="col-department">담당부서</th>
|
|
<th class="col-person">담당자</th>
|
|
<th class="col-date">조치예상일</th>
|
|
<th class="col-completion">완료 확인</th>
|
|
<th class="col-photos">업로드 사진</th>
|
|
<th class="col-actions">작업</th>
|
|
`;
|
|
} else {
|
|
// 완료됨 탭 헤더
|
|
return `
|
|
<th class="col-no">
|
|
<span class="inline-flex items-center space-x-1">
|
|
<span class="font-bold bg-gradient-to-r from-green-600 to-green-800 bg-clip-text text-transparent">No.</span>
|
|
<div class="w-1.5 h-1.5 bg-green-500 rounded-full"></div>
|
|
</span>
|
|
</th>
|
|
<th class="col-project">프로젝트</th>
|
|
<th class="col-content">내용</th>
|
|
<th class="col-cause">원인</th>
|
|
<th class="col-solution">해결방안</th>
|
|
<th class="col-department">담당부서</th>
|
|
<th class="col-person">담당자</th>
|
|
<th class="col-date">조치예상일</th>
|
|
<th class="col-confirmer">확인자</th>
|
|
<th class="col-department">원인부서</th>
|
|
<th class="col-comment">의견</th>
|
|
<th class="col-photos">업로드 사진</th>
|
|
<th class="col-completion">완료 사진</th>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 편집 가능한 필드 생성 함수
|
|
function createEditableField(fieldName, value, type, issueId, editable, options = null) {
|
|
if (!editable) {
|
|
return value || '-';
|
|
}
|
|
|
|
const fieldId = `${fieldName}_${issueId}`;
|
|
|
|
switch (type) {
|
|
case 'textarea':
|
|
return `<textarea id="${fieldId}" class="editable-field text-wrap" rows="2">${value}</textarea>`;
|
|
case 'select':
|
|
if (options) {
|
|
const optionsHtml = options.map(opt =>
|
|
`<option value="${opt.value}" ${opt.value === value ? 'selected' : ''}>${opt.text}</option>`
|
|
).join('');
|
|
return `<select id="${fieldId}" class="editable-field">${optionsHtml}</select>`;
|
|
}
|
|
break;
|
|
case 'date':
|
|
return `<input type="date" id="${fieldId}" class="editable-field" value="${value}">`;
|
|
case 'text':
|
|
default:
|
|
return `<input type="text" id="${fieldId}" class="editable-field" value="${value}">`;
|
|
}
|
|
|
|
return value || '-';
|
|
}
|
|
|
|
// 부서 옵션 생성 함수
|
|
function getDepartmentOptions() {
|
|
return [
|
|
{ value: '', text: '선택하세요' },
|
|
{ value: 'production', text: '생산' },
|
|
{ value: 'quality', text: '품질' },
|
|
{ value: 'purchasing', text: '구매' },
|
|
{ value: 'design', text: '설계' },
|
|
{ value: 'sales', text: '영업' }
|
|
];
|
|
}
|
|
|
|
// 날짜 그룹 토글 함수
|
|
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);
|
|
}
|
|
|
|
|
|
|
|
// 상태 변경 모달
|
|
function openStatusModal(issueId) {
|
|
currentIssueId = issueId;
|
|
document.getElementById('statusModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeStatusModal() {
|
|
currentIssueId = null;
|
|
document.getElementById('statusModal').classList.add('hidden');
|
|
document.getElementById('newStatus').value = 'processing';
|
|
document.getElementById('statusNote').value = '';
|
|
}
|
|
|
|
async function updateStatus() {
|
|
if (!currentIssueId) return;
|
|
|
|
const newStatus = document.getElementById('newStatus').value;
|
|
const note = document.getElementById('statusNote').value;
|
|
|
|
try {
|
|
const response = await fetch(`/api/issues/${currentIssueId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
status: newStatus,
|
|
note: note
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
await loadIssues();
|
|
closeStatusModal();
|
|
alert('상태가 성공적으로 변경되었습니다.');
|
|
} else {
|
|
throw new Error('상태 변경에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('상태 변경 실패:', error);
|
|
alert('상태 변경에 실패했습니다.');
|
|
}
|
|
}
|
|
|
|
// 완료 처리 함수
|
|
async function completeIssue(issueId) {
|
|
if (!confirm('이 부적합을 완료 처리하시겠습니까?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/inbox/${issueId}/status`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
review_status: 'completed'
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
alert('완료 처리되었습니다.');
|
|
await loadIssues(); // 목록 새로고침
|
|
} else {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || '완료 처리에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('완료 처리 실패:', error);
|
|
alert(error.message || '완료 처리 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
// 이슈 변경사항 저장 함수
|
|
async function saveIssueChanges(issueId) {
|
|
try {
|
|
// 편집된 필드들의 값 수집
|
|
const updates = {};
|
|
const fields = ['solution', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department', 'management_comment'];
|
|
|
|
fields.forEach(field => {
|
|
const element = document.getElementById(`${field}_${issueId}`);
|
|
if (element) {
|
|
let value = element.value.trim();
|
|
if (value === '' || value === '선택하세요') {
|
|
value = null;
|
|
} else if (field === 'expected_completion_date' && value) {
|
|
// 날짜 필드는 ISO datetime 형식으로 변환
|
|
value = value + 'T00:00:00';
|
|
}
|
|
updates[field] = value;
|
|
}
|
|
});
|
|
|
|
console.log('Sending updates:', updates);
|
|
|
|
// API 호출
|
|
const response = await fetch(`/api/issues/${issueId}/management`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(updates)
|
|
});
|
|
|
|
console.log('Response status:', response.status);
|
|
|
|
if (response.ok) {
|
|
alert('변경사항이 저장되었습니다.');
|
|
await loadIssues(); // 목록 새로고침
|
|
} else {
|
|
const errorText = await response.text();
|
|
console.error('API Error Response:', errorText);
|
|
let errorMessage = '저장에 실패했습니다.';
|
|
try {
|
|
const errorJson = JSON.parse(errorText);
|
|
errorMessage = errorJson.detail || JSON.stringify(errorJson);
|
|
} catch (e) {
|
|
errorMessage = errorText || '저장에 실패했습니다.';
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
} catch (error) {
|
|
console.error('저장 실패:', error);
|
|
console.error('Error details:', error);
|
|
alert(error.message || '저장 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
// 완료된 이슈 상세보기 모달 함수들
|
|
let currentModalIssueId = null;
|
|
|
|
async function openIssueDetailModal(issueId) {
|
|
currentModalIssueId = issueId;
|
|
const issue = issues.find(i => i.id === issueId);
|
|
if (!issue) return;
|
|
|
|
const project = projects.find(p => p.id === issue.project_id);
|
|
|
|
// 모달 제목 설정
|
|
document.getElementById('modalTitle').innerHTML = `
|
|
<span class="inline-flex items-center space-x-2">
|
|
<span>부적합</span>
|
|
<span class="text-xl font-bold bg-gradient-to-r from-green-600 to-green-800 bg-clip-text text-transparent">No.${issue.project_sequence_no || '-'}</span>
|
|
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
|
<span>상세 정보</span>
|
|
</span>
|
|
`;
|
|
|
|
// 모달 내용 생성
|
|
const modalContent = document.getElementById('modalContent');
|
|
modalContent.innerHTML = createModalContent(issue, project);
|
|
|
|
// 모달 표시
|
|
document.getElementById('issueDetailModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeIssueDetailModal() {
|
|
document.getElementById('issueDetailModal').classList.add('hidden');
|
|
currentModalIssueId = null;
|
|
}
|
|
|
|
function createModalContent(issue, project) {
|
|
return `
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- 읽기 전용 필드들 -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2">기본 정보 (수신함 확정)</h3>
|
|
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
|
|
<div class="p-3 bg-gray-50 rounded-lg text-gray-800">${project ? project.project_name : '-'}</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">내용</label>
|
|
<div class="p-3 bg-gray-50 rounded-lg text-gray-800 whitespace-pre-wrap">${issue.final_description || issue.description}</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">원인</label>
|
|
<div class="p-3 bg-gray-50 rounded-lg text-gray-800">${getCategoryText(issue.final_category || issue.category)}</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">확인자</label>
|
|
<div class="p-3 bg-gray-50 rounded-lg text-gray-800">${getReporterNames(issue)}</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">업로드 사진</label>
|
|
<div class="flex gap-2">
|
|
${issue.photo_path ? `<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded cursor-pointer" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진 1">` : '<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">없음</div>'}
|
|
${issue.photo_path2 ? `<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded cursor-pointer" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : '<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">없음</div>'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 편집 가능한 필드들 -->
|
|
<div class="space-y-4">
|
|
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2">관리 정보 (편집 가능)</h3>
|
|
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">해결방안</label>
|
|
<textarea id="modal_solution" rows="3" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">${issue.solution || ''}</textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">담당부서</label>
|
|
<select id="modal_responsible_department" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
${getDepartmentOptions().map(opt =>
|
|
`<option value="${opt.value}" ${opt.value === (issue.responsible_department || '') ? 'selected' : ''}>${opt.text}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">담당자</label>
|
|
<input type="text" id="modal_responsible_person" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="${issue.responsible_person || ''}">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">조치예상일</label>
|
|
<input type="date" id="modal_expected_completion_date" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value="${issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : ''}">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">원인부서</label>
|
|
<select id="modal_cause_department" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
${getDepartmentOptions().map(opt =>
|
|
`<option value="${opt.value}" ${opt.value === (issue.cause_department || '') ? 'selected' : ''}>${opt.text}</option>`
|
|
).join('')}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">의견</label>
|
|
<textarea id="modal_management_comment" rows="3" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">${issue.management_comment || ''}</textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">완료 사진</label>
|
|
<div class="flex items-center gap-3">
|
|
${issue.completion_photo_path ?
|
|
`<img src="${issue.completion_photo_path}" class="w-20 h-20 object-cover rounded cursor-pointer" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">` :
|
|
'<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">없음</div>'
|
|
}
|
|
<input type="file" id="modal_completion_photo" accept="image/*" class="flex-1 text-sm">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function saveModalChanges() {
|
|
if (!currentModalIssueId) return;
|
|
|
|
try {
|
|
// 편집된 필드들의 값 수집
|
|
const updates = {};
|
|
const fields = ['solution', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department', 'management_comment'];
|
|
|
|
fields.forEach(field => {
|
|
const element = document.getElementById(`modal_${field}`);
|
|
if (element) {
|
|
let value = element.value.trim();
|
|
if (value === '' || value === '선택하세요') {
|
|
value = null;
|
|
}
|
|
updates[field] = value;
|
|
}
|
|
});
|
|
|
|
// 완료 사진 처리
|
|
const photoFile = document.getElementById('modal_completion_photo').files[0];
|
|
if (photoFile) {
|
|
const base64 = await fileToBase64(photoFile);
|
|
updates.completion_photo = base64;
|
|
}
|
|
|
|
console.log('Modal sending updates:', updates);
|
|
|
|
// API 호출
|
|
const response = await fetch(`/api/issues/${currentModalIssueId}/management`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(updates)
|
|
});
|
|
|
|
console.log('Modal response status:', response.status);
|
|
|
|
if (response.ok) {
|
|
alert('변경사항이 저장되었습니다.');
|
|
closeIssueDetailModal();
|
|
await loadIssues(); // 목록 새로고침
|
|
} else {
|
|
const errorText = await response.text();
|
|
console.error('Modal API Error Response:', errorText);
|
|
let errorMessage = '저장에 실패했습니다.';
|
|
try {
|
|
const errorJson = JSON.parse(errorText);
|
|
errorMessage = errorJson.detail || JSON.stringify(errorJson);
|
|
} catch (e) {
|
|
errorMessage = errorText || '저장에 실패했습니다.';
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
} catch (error) {
|
|
console.error('모달 저장 실패:', error);
|
|
console.error('Error details:', error);
|
|
alert(error.message || '저장 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
// 파일을 Base64로 변환하는 함수
|
|
function fileToBase64(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file);
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = error => reject(error);
|
|
});
|
|
}
|
|
|
|
// 기타 함수들
|
|
|
|
function viewIssueDetail(issueId) {
|
|
window.location.href = `/issue-view.html#detail-${issueId}`;
|
|
}
|
|
|
|
// 유틸리티 함수들
|
|
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 getStatusBadgeClass(status) {
|
|
const statusMap = {
|
|
'new': 'new',
|
|
'processing': 'processing',
|
|
'pending': 'pending',
|
|
'completed': 'completed'
|
|
};
|
|
return statusMap[status] || 'new';
|
|
}
|
|
|
|
function getStatusText(status) {
|
|
const statusMap = {
|
|
'new': '새 부적합',
|
|
'processing': '처리 중',
|
|
'pending': '대기 중',
|
|
'completed': '완료'
|
|
};
|
|
return statusMap[status] || status;
|
|
}
|
|
|
|
function getPriorityBadge(priority) {
|
|
const priorityMap = {
|
|
'high': { text: '높음', class: 'bg-red-100 text-red-800' },
|
|
'medium': { text: '보통', class: 'bg-yellow-100 text-yellow-800' },
|
|
'low': { text: '낮음', class: 'bg-green-100 text-green-800' }
|
|
};
|
|
const p = priorityMap[priority] || { text: '보통', class: 'bg-gray-100 text-gray-800' };
|
|
return `<span class="badge ${p.class}">${p.text}</span>`;
|
|
}
|
|
|
|
function getCategoryText(category) {
|
|
const categoryMap = {
|
|
'material_missing': '자재 누락',
|
|
'design_error': '설계 오류',
|
|
'incoming_defect': '반입 불량',
|
|
'inspection_miss': '검사 누락',
|
|
'etc': '기타'
|
|
};
|
|
return categoryMap[category] || category;
|
|
}
|
|
|
|
// API 스크립트 동적 로딩
|
|
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
|
|
const script = document.createElement('script');
|
|
script.src = `/static/js/api.js?v=20251025-2&cb=${cacheBuster}&t=${Date.now()}&r=${Math.random()}`;
|
|
script.setAttribute('cache-control', 'no-cache');
|
|
script.setAttribute('pragma', 'no-cache');
|
|
script.onload = function() {
|
|
console.log('✅ API 스크립트 로드 완료 (issues-management.html)');
|
|
initializeManagement();
|
|
};
|
|
script.onerror = function() {
|
|
console.error('❌ API 스크립트 로드 실패');
|
|
};
|
|
document.head.appendChild(script);
|
|
|
|
// 추가 정보 모달 관련 함수들
|
|
let selectedIssueId = null;
|
|
|
|
function openAdditionalInfoModal() {
|
|
// 진행 중 탭에서 선택된 이슈가 있는지 확인
|
|
const inProgressIssues = allIssues.filter(issue => issue.review_status === 'in_progress');
|
|
|
|
if (inProgressIssues.length === 0) {
|
|
alert('진행 중인 부적합이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
// 첫 번째 진행 중 이슈를 기본 선택 (추후 개선 가능)
|
|
selectedIssueId = inProgressIssues[0].id;
|
|
|
|
// 기존 데이터 로드
|
|
loadAdditionalInfo(selectedIssueId);
|
|
|
|
document.getElementById('additionalInfoModal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeAdditionalInfoModal() {
|
|
document.getElementById('additionalInfoModal').classList.add('hidden');
|
|
selectedIssueId = null;
|
|
|
|
// 폼 초기화
|
|
document.getElementById('additionalInfoForm').reset();
|
|
}
|
|
|
|
async function loadAdditionalInfo(issueId) {
|
|
try {
|
|
const response = await fetch(`/api/management/${issueId}/additional-info`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
// 폼에 기존 데이터 채우기
|
|
document.getElementById('causeDepartment').value = data.cause_department || '';
|
|
document.getElementById('responsiblePersonDetail').value = data.responsible_person_detail || '';
|
|
document.getElementById('causeDetail').value = data.cause_detail || '';
|
|
}
|
|
} catch (error) {
|
|
console.error('추가 정보 로드 실패:', error);
|
|
}
|
|
}
|
|
|
|
// 추가 정보 폼 제출 처리
|
|
document.getElementById('additionalInfoForm').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
if (!selectedIssueId) {
|
|
alert('선택된 부적합이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
const formData = {
|
|
cause_department: document.getElementById('causeDepartment').value || null,
|
|
responsible_person_detail: document.getElementById('responsiblePersonDetail').value || null,
|
|
cause_detail: document.getElementById('causeDetail').value || null
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`/api/management/${selectedIssueId}/additional-info`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
alert('추가 정보가 성공적으로 저장되었습니다.');
|
|
closeAdditionalInfoModal();
|
|
|
|
// 목록 새로고침
|
|
loadIssues();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`저장 실패: ${error.detail || '알 수 없는 오류'}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('추가 정보 저장 실패:', error);
|
|
alert('저장 중 오류가 발생했습니다.');
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<!-- 추가 정보 입력 모달 -->
|
|
<div id="additionalInfoModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
|
|
<div class="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
<div class="p-6">
|
|
<!-- 모달 헤더 -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h3 class="text-lg font-semibold text-gray-900">
|
|
<i class="fas fa-info-circle text-orange-500 mr-2"></i>
|
|
추가 정보 입력
|
|
</h3>
|
|
<button onclick="closeAdditionalInfoModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
|
|
<i class="fas fa-times text-xl"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 모달 내용 -->
|
|
<form id="additionalInfoForm" class="space-y-4">
|
|
<!-- 원인부서 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
<i class="fas fa-building text-gray-500 mr-1"></i>
|
|
원인부서
|
|
</label>
|
|
<select id="causeDepartment" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500">
|
|
<option value="">선택하세요</option>
|
|
<option value="production">생산</option>
|
|
<option value="quality">품질</option>
|
|
<option value="purchasing">구매</option>
|
|
<option value="design">설계</option>
|
|
<option value="sales">영업</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 해당자 상세 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
<i class="fas fa-user text-gray-500 mr-1"></i>
|
|
해당자 상세
|
|
</label>
|
|
<input type="text" id="responsiblePersonDetail"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500"
|
|
placeholder="해당자 이름, 직책 등 상세 정보">
|
|
</div>
|
|
|
|
<!-- 원인 상세 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
<i class="fas fa-clipboard-list text-gray-500 mr-1"></i>
|
|
원인 상세
|
|
</label>
|
|
<textarea id="causeDetail" rows="4"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 resize-none"
|
|
placeholder="원인에 대한 상세한 설명을 입력하세요"></textarea>
|
|
</div>
|
|
|
|
<!-- 안내 메시지 -->
|
|
<div class="bg-orange-50 border border-orange-200 rounded-lg p-3">
|
|
<div class="flex items-start">
|
|
<i class="fas fa-info-circle text-orange-500 mt-0.5 mr-2"></i>
|
|
<div class="text-sm text-orange-700">
|
|
<p class="font-medium mb-1">기록용 정보</p>
|
|
<p>이 정보는 내부 기록용으로만 사용되며, 모든 필드는 선택사항입니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 버튼 -->
|
|
<div class="flex space-x-3 pt-4">
|
|
<button type="button" onclick="closeAdditionalInfoModal()"
|
|
class="flex-1 px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
|
|
취소
|
|
</button>
|
|
<button type="submit"
|
|
class="flex-1 px-4 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors">
|
|
<i class="fas fa-save mr-2"></i>저장
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|