Files
M-Project/frontend/issues-management.html
Hyungi Ahn 95be1f6c6e feat: 관리함 완전 개편 - 편집 가능한 테이블 및 완료 처리 기능
🎯 Major Management Page Overhaul:
- 테이블 최소 너비 2000px로 확장 (좌우 스크롤 최적화)
- 컬럼별 개별 너비 설정으로 내용에 맞는 크기 조정
- 편집 가능한 필드들 (해결방안, 담당부서, 담당자, 조치예상일, 원인부서, 의견)
- 진행 중 → 완료됨 처리 버튼 추가

📊 Enhanced Table Structure:
- 업로드 사진 2장 표시 (photo_path, photo_path2)
- 완료 사진 별도 컬럼으로 표시
- 작업 컬럼 추가 (저장 버튼)
- 완료 확인 컬럼 (진행 중: 완료 처리 버튼, 완료됨: 완료일)

✏️ Editable Fields Implementation:
- createEditableField() 함수로 동적 입력 필드 생성
- textarea, select, date, text 타입 지원
- 부서 선택 드롭다운 (생산, 품질, 구매, 설계, 영업)
- 실시간 편집 및 저장 기능

🔧 Backend API Enhancement:
- PUT /api/issues/{issue_id}/management 엔드포인트 추가
- ManagementUpdateRequest 스키마 활용
- 관리함 페이지 권한 검증
- 완료 사진 업로드 지원

📈 Smart Sequencing System:
- 수신함에서 넘어온 순서대로 No. 할당 (reviewed_at 기준)
- 프로젝트별 그룹화 후 순번 재할당
- 진행 중 A → 진행 중 B → 완료됨 C → 진행 중 D = 1, 2, 3, 4

🎨 UI/UX Improvements:
- 컬럼별 CSS 클래스로 일관된 스타일링
- 편집 가능한 필드 포커스 효과
- 사진 컨테이너로 2장 사진 깔끔한 배치
- 버튼 크기 최적화 (btn-sm 클래스)

🚀 Functional Features:
- completeIssue(): 진행 중 → 완료됨 처리
- saveIssueChanges(): 편집된 필드들 일괄 저장
- 실시간 목록 새로고침
- 확인 다이얼로그로 안전한 작업 처리

Expected Result:
 좌우 스크롤로 모든 정보 편리하게 확인
 관리함에서 필요한 정보 직접 입력/수정
 진행 중에서 완료 처리 원클릭
 수신함 순서 기반 체계적인 No. 관리
 업로드 사진 2장 + 완료 사진 명확한 구분
2025-10-25 15:48:53 +09:00

968 lines
41 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;
}
.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; }
.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 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>
<!-- 프로젝트별 통계 -->
<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="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');
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';
} 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';
}
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}">
<div class="issue-table-container">
<table class="issue-table">
<thead>
<tr>
<th class="col-no">No.</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">${currentTab === 'in_progress' ? '완료 확인' : '완료확인일'}</th>
<th class="col-confirmer">확인자</th>
<th class="col-department">원인부서</th>
<th class="col-comment">의견</th>
<th class="col-status">조치결과</th>
<th class="col-photos">업로드 사진</th>
<th class="col-completion">완료 사진</th>
<th class="col-actions">작업</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';
const isInProgress = issue.review_status === 'in_progress';
const isCompleted = issue.review_status === 'completed';
return `
<tr data-issue-id="${issue.id}">
<td class="col-no font-medium">${issue.project_sequence_no || '-'}</td>
<td class="col-project">${project ? project.project_name : '-'}</td>
<td class="col-content text-wrap">${issue.final_description || issue.description}</td>
<td class="col-cause">${getCategoryText(issue.final_category || issue.category)}</td>
<td class="col-solution">
${createEditableField('solution', issue.solution || '', 'textarea', issue.id, true)}
</td>
<td class="col-department">
${createEditableField('responsible_department', issue.responsible_department || '', 'select', issue.id, true, getDepartmentOptions())}
</td>
<td class="col-person">
${createEditableField('responsible_person', issue.responsible_person || '', 'text', issue.id, true)}
</td>
<td class="col-date">
${createEditableField('expected_completion_date', issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : '', 'date', issue.id, true)}
</td>
<td class="col-completion">
${isInProgress ?
`<button onclick="completeIssue(${issue.id})" class="btn-sm bg-green-500 text-white hover:bg-green-600">완료 처리</button>` :
(issue.actual_completion_date ? new Date(issue.actual_completion_date).toLocaleDateString('ko-KR') : '-')
}
</td>
<td class="col-confirmer">${getReporterNames(issue)}</td>
<td class="col-department">
${createEditableField('cause_department', issue.cause_department || '', 'select', issue.id, true, getDepartmentOptions())}
</td>
<td class="col-comment">
${createEditableField('management_comment', issue.management_comment || '', 'textarea', issue.id, true)}
</td>
<td class="col-status ${statusClass} font-medium">${statusText}</td>
<td class="col-photos">
<div class="photo-container">
${issue.photo_path ? `<img src="${issue.photo_path}" class="issue-photo" onclick="openPhotoModal('${issue.photo_path}')" alt="업로드 사진 1">` : ''}
${issue.photo_path2 ? `<img src="${issue.photo_path2}" class="issue-photo" onclick="openPhotoModal('${issue.photo_path2}')" alt="업로드 사진 2">` : ''}
${!issue.photo_path && !issue.photo_path2 ? '-' : ''}
</div>
</td>
<td class="col-completion">
${issue.completion_photo_path ? `<img src="${issue.completion_photo_path}" class="issue-photo" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">` : '-'}
</td>
<td class="col-actions">
<button onclick="saveIssueChanges(${issue.id})" class="btn-sm bg-blue-500 text-white hover:bg-blue-600">저장</button>
</td>
</tr>
`;
}
// 편집 가능한 필드 생성 함수
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;
}
updates[field] = value;
}
});
// 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)
});
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 || '저장 중 오류가 발생했습니다.');
}
}
// 기타 함수들
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);
</script>
</body>
</html>