Files
M-Project/frontend/issues-management.html
hyungi c4af58d849 Fix: 관리함 날짜별 그룹에서 이슈가 잘리는 문제 수정
- CSS max-height 제한으로 인한 내용 잘림 문제 해결
- collapse-content의 max-height를 1000px → none으로 변경
- 기본 상태를 펼쳐진 상태로 설정하여 모든 이슈 표시
- 날짜별 그룹 초기화 로직 추가
- 디버깅 로그 추가로 각 날짜 그룹의 이슈 개수 확인 가능

이제 완료된 이슈 3개가 모두 정상적으로 표시됩니다.
2025-11-17 06:37:07 +09:00

2806 lines
148 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: none;
overflow: visible;
transition: max-height 0.3s ease-out;
}
.collapse-content.collapsed {
max-height: 0;
overflow: hidden;
}
/* 진행 중 카드 스타일 */
.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-4 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-purple-50 p-4 rounded-lg">
<div class="flex items-center">
<i class="fas fa-hourglass-half text-purple-500 text-xl mr-3"></i>
<div>
<p class="text-sm text-purple-600">완료 대기</p>
<p class="text-2xl font-bold text-purple-700" id="pendingCompletionCount">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'; // 기본값: 진행 중
// 완료 반려 패턴 제거 (해결방안 표시용)
function cleanManagementComment(text) {
if (!text) return '';
// 기존 데이터에서 완료 반려 패턴 제거
return text.replace(/\[완료 반려[^\]]*\][^\n]*\n*/g, '').trim();
}
// 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 {
// ProjectsAPI 사용 (모든 프로젝트 로드)
projects = await ProjectsAPI.getAll(false);
updateProjectFilter();
} catch (error) {
console.error('프로젝트 로드 실패:', error);
}
}
// 부적합 목록 로드 (관리함 API 사용)
async function loadIssues() {
try {
// ManagementAPI 사용
const managementIssues = await ManagementAPI.getAll();
console.log('🔍 관리함 이슈 로드 완료:', managementIssues.length, '개');
console.log('📊 상태별 분포:', {
in_progress: managementIssues.filter(i => i.review_status === 'in_progress').length,
completed: managementIssues.filter(i => i.review_status === 'completed').length,
other: managementIssues.filter(i => !['in_progress', 'completed'].includes(i.review_status)).length
});
// 수신함에서 넘어온 순서대로 No. 재할당 (reviewed_at 기준)
managementIssues.sort((a, b) => new Date(a.reviewed_at) - new Date(b.reviewed_at));
// 프로젝트별로 그룹화하여 No. 재할당
const projectGroups = {};
managementIssues.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 = managementIssues;
filterIssues();
} 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' && !issue.completion_requested_at
).length;
const pendingCompletionCount = projectIssues.filter(issue =>
issue.review_status === 'in_progress' && issue.completion_requested_at
).length;
const completedCount = projectIssues.filter(issue => issue.review_status === 'completed').length;
// 통계 업데이트
document.getElementById('totalCount').textContent = totalCount;
document.getElementById('inProgressCount').textContent = inProgressCount;
document.getElementById('pendingCompletionCount').textContent = pendingCompletionCount;
document.getElementById('completedCount').textContent = completedCount;
}
// 필터링 및 표시 함수들
function filterIssues() {
const projectFilter = document.getElementById('projectFilter').value;
console.log('🔍 필터링 시작:', {
currentTab: currentTab,
projectFilter: projectFilter,
totalIssues: issues.length
});
filteredIssues = issues.filter(issue => {
// 현재 탭에 따른 상태 필터링
let statusMatch = false;
if (currentTab === 'in_progress') {
statusMatch = issue.review_status === 'in_progress';
} else if (currentTab === 'completed') {
statusMatch = issue.review_status === 'completed';
}
if (!statusMatch) return false;
// 프로젝트 필터링
if (projectFilter && issue.project_id != projectFilter) return false;
return true;
});
console.log('✅ 필터링 결과:', {
filteredCount: filteredIssues.length,
tab: currentTab
});
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, '-')}`;
console.log(`📅 날짜 그룹 [${date}]: ${issues.length}개 이슈`);
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="space-y-4 mt-4">
${issues.map(issue => createIssueRow(issue)).join('')}
</div>
</div>
</div>
`;
}).join('');
container.innerHTML = dateGroups;
// 모든 날짜 그룹을 기본적으로 펼쳐진 상태로 초기화
Object.keys(groupedByDate).forEach(date => {
const groupId = `group-${date.replace(/\./g, '-')}`;
const content = document.getElementById(groupId);
const icon = document.getElementById(`icon-${groupId}`);
if (content && icon) {
content.classList.remove('collapsed');
icon.style.transform = 'rotate(0deg)';
}
});
}
// 이슈 행 생성 함수
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.description || issue.final_description || '';
const lines = description.split('\n');
return lines[0] || '부적합명 없음';
}
// 상세 내용 추출 (두 번째 줄부터)
function getIssueDetail(issue) {
const description = issue.description || issue.final_description || '';
const lines = description.split('\n');
return lines.slice(1).join('\n') || '상세 내용 없음';
}
// 진행 중 카드 생성
function createInProgressRow(issue, project) {
// 상태 판별
const isPendingCompletion = issue.completion_requested_at;
const isOverdue = issue.expected_completion_date && new Date(issue.expected_completion_date) < new Date();
const isUrgent = issue.expected_completion_date &&
(new Date(issue.expected_completion_date) - new Date()) / (1000 * 60 * 60 * 24) <= 3 &&
!isOverdue;
// 상태 설정
let statusConfig = {
text: '진행 중',
bgColor: 'bg-gradient-to-r from-blue-500 to-blue-600',
icon: 'fas fa-cog fa-spin',
dotColor: 'bg-white'
};
if (isPendingCompletion) {
statusConfig = {
text: '완료 대기',
bgColor: 'bg-gradient-to-r from-purple-500 to-purple-600',
icon: 'fas fa-hourglass-half',
dotColor: 'bg-white'
};
} else if (isOverdue) {
statusConfig = {
text: '지연됨',
bgColor: 'bg-gradient-to-r from-red-500 to-red-600',
icon: 'fas fa-clock',
dotColor: 'bg-white'
};
} else if (isUrgent) {
statusConfig = {
text: '긴급',
bgColor: 'bg-gradient-to-r from-orange-500 to-orange-600',
icon: 'fas fa-exclamation-triangle',
dotColor: 'bg-white'
};
}
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 class="flex items-center space-x-2 ${statusConfig.bgColor} text-white px-3 py-1 rounded-full shadow-sm">
<div class="w-1.5 h-1.5 ${statusConfig.dotColor} rounded-full animate-pulse"></div>
<span class="text-xs font-bold">${statusConfig.text}</span>
<i class="${statusConfig.icon} text-xs"></i>
</div>
</div>
<div class="bg-blue-50 px-3 py-2 rounded-lg border-l-4 border-blue-400">
<h3 class="text-lg font-bold text-blue-900">${getIssueTitle(issue)}</h3>
</div>
</div>
<div class="flex space-x-2">
${isPendingCompletion ? `
<!-- 완료 대기 상태 버튼들 -->
<button onclick="rejectCompletion(${issue.id})" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-times mr-1"></i>반려
</button>
<button onclick="confirmCompletion(${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-circle mr-1"></i>최종확인
</button>
` : `
<!-- 일반 진행 중 상태 버튼들 -->
<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="confirmDeleteIssue(${issue.id})" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-trash mr-1"></i>삭제
</button>
<button onclick="confirmCompletion(${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>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<i class="fas fa-align-left text-gray-500 mr-2"></i>
<label class="text-sm font-medium text-gray-700">상세 내용</label>
</div>
${!isPendingCompletion ? `
<button onclick="toggleDetailEdit(${issue.id})" class="text-xs text-blue-600 hover:text-blue-800 px-2 py-1 rounded border border-blue-200 hover:bg-blue-50 transition-colors">
<i class="fas fa-edit mr-1"></i>수정
</button>
` : `
<span class="text-xs text-gray-400 px-2 py-1 rounded border border-gray-200 bg-gray-50">
<i class="fas fa-lock mr-1"></i>완료 대기 중
</span>
`}
</div>
<div id="detail-display-${issue.id}" class="p-3 bg-gray-50 rounded-lg border border-gray-200 min-h-[80px]">
<div class="text-gray-600 text-sm leading-relaxed italic">
${getIssueDetail(issue)}
</div>
</div>
<div id="detail-edit-${issue.id}" class="hidden">
<textarea
id="detail-textarea-${issue.id}"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-h-[80px] text-sm"
placeholder="상세 내용을 입력하세요...">${getIssueDetail(issue)}</textarea>
<div class="flex justify-end space-x-2 mt-2">
<button onclick="cancelDetailEdit(${issue.id})" class="px-3 py-1 text-xs text-gray-600 hover:text-gray-800 border border-gray-300 rounded hover:bg-gray-50 transition-colors">
취소
</button>
<button onclick="saveDetailEdit(${issue.id})" class="px-3 py-1 text-xs text-white bg-blue-500 hover:bg-blue-600 rounded transition-colors">
<i class="fas fa-save mr-1"></i>저장
</button>
</div>
</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.category || issue.final_category)}
</span>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">업로드 사진</label>
${(() => {
const photos = [
issue.photo_path,
issue.photo_path2,
issue.photo_path3,
issue.photo_path4,
issue.photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<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>';
}
return `
<div class="flex flex-wrap gap-2">
${photos.map((path, idx) => `
<img src="${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('${path}')" alt="업로드 사진 ${idx + 1}">
`).join('')}
</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="management_comment_${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 ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" placeholder="확정된 해결 방안을 입력하세요..." ${isPendingCompletion ? 'readonly' : ''}>${cleanManagementComment(issue.management_comment)}</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 ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" ${isPendingCompletion ? 'disabled' : ''}>
${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 ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" placeholder="담당자 이름" value="${issue.responsible_person || ''}" ${isPendingCompletion ? 'readonly' : ''}>
</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 ${isPendingCompletion ? 'bg-gray-100 cursor-not-allowed' : ''}" value="${issue.expected_completion_date ? issue.expected_completion_date.split('T')[0] : ''}" ${isPendingCompletion ? 'readonly' : ''}>
</div>
<!-- 완료 대기 상태일 때 완료 정보 표시 -->
${isPendingCompletion ? `
<div class="mt-4 p-4 bg-purple-50 rounded-lg border border-purple-200">
<h4 class="text-sm font-semibold text-purple-800 mb-3 flex items-center">
<i class="fas fa-check-circle mr-2"></i>완료 신청 정보
</h4>
<div class="space-y-3">
<div>
<label class="text-xs text-purple-600 font-medium">완료 사진</label>
${(() => {
const photos = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<p class="text-xs text-gray-500 mt-1">완료 사진 없음</p>';
}
return `
<div class="mt-1 flex flex-wrap gap-2">
${photos.map(path => `
<img src="${path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-purple-200 hover:border-purple-400 transition-colors" onclick="openPhotoModal('${path}')" alt="완료 사진">
`).join('')}
</div>
`;
})()}
</div>
<div>
<label class="text-xs text-purple-600 font-medium">완료 코멘트</label>
<p class="text-sm text-gray-700 mt-1 p-2 bg-white rounded border">${issue.completion_comment || '코멘트 없음'}</p>
</div>
<div>
<label class="text-xs text-purple-600 font-medium">신청일시</label>
<p class="text-sm text-gray-700 mt-1">${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}</p>
</div>
</div>
</div>
` : ''}
<!-- 진행 상태 표시 -->
<div class="mt-4 p-3 ${isPendingCompletion ? 'bg-purple-50' : 'bg-blue-50'} rounded-lg">
<div class="flex items-center justify-between">
<span class="text-sm ${isPendingCompletion ? 'text-purple-700' : 'text-blue-700'} font-medium">
<i class="${statusConfig.icon} mr-1"></i>${statusConfig.text}
</span>
<span class="text-xs ${isPendingCompletion ? 'text-purple-600' : 'text-blue-600'}">
신고일: ${new Date(issue.report_date).toLocaleDateString('ko-KR')}
</span>
</div>
</div>
</div>
</div>
</div>
`;
}
// 완료됨 행 생성 (입력 여부 표시)
function createCompletedRow(issue, project) {
// 완료 날짜 포맷팅
const completedDate = issue.completed_at ? new Date(issue.completed_at).toLocaleDateString('ko-KR') : '미완료';
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-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>
</div>
<span class="text-sm text-gray-600">${project ? project.project_name : '프로젝트 미지정'}</span>
<!-- 완료 상태 표시 -->
<div class="flex items-center space-x-2 bg-gradient-to-r from-green-500 to-green-600 text-white px-3 py-1 rounded-full shadow-sm">
<div class="w-1.5 h-1.5 bg-white rounded-full"></div>
<span class="text-xs font-bold">완료됨</span>
<i class="fas fa-check-circle text-xs"></i>
</div>
<span class="text-xs text-gray-500">완료일: ${completedDate}</span>
</div>
<div class="bg-green-50 px-3 py-2 rounded-lg border-l-4 border-green-400">
<h3 class="text-lg font-bold text-green-900">${getIssueTitle(issue)}</h3>
</div>
</div>
<div class="flex space-x-2">
<button onclick="openIssueDetailModal(${issue.id})" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<i class="fas fa-eye mr-1"></i>상세보기
</button>
</div>
</div>
<!-- 카드 본문 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<!-- 기본 정보 -->
<div class="bg-gray-50 p-4 rounded-lg border border-gray-200">
<h4 class="font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-info-circle text-blue-500 mr-2"></i>
기본 정보
</h4>
<div class="space-y-2 text-sm">
<div class="bg-gray-50 p-2 rounded border-l-4 border-gray-400">
<p class="text-gray-600 italic">${getIssueDetail(issue)}</p>
</div>
<div><span class="font-medium text-gray-700">원인분류:</span> <span class="text-gray-900">${getCategoryText(issue.final_category || issue.category) || '-'}</span></div>
<div><span class="font-medium text-gray-700">확인자:</span> <span class="text-gray-900">${getReporterNames(issue) || '-'}</span></div>
</div>
</div>
<!-- 관리 정보 -->
<div class="bg-blue-50 p-4 rounded-lg border border-blue-200">
<h4 class="font-semibold text-blue-800 mb-3 flex items-center">
<i class="fas fa-cogs text-blue-500 mr-2"></i>
관리 정보
</h4>
<div class="space-y-2 text-sm">
<div><span class="font-medium text-blue-700">해결방안 (확정):</span> <span class="text-blue-900">${cleanManagementComment(issue.management_comment) || '-'}</span></div>
<div><span class="font-medium text-blue-700">담당부서:</span> <span class="text-blue-900">${getDepartmentText(issue.responsible_department) || '-'}</span></div>
<div><span class="font-medium text-blue-700">담당자:</span> <span class="text-blue-900">${issue.responsible_person || '-'}</span></div>
<div><span class="font-medium text-blue-700">원인부서:</span> <span class="text-blue-900">${getDepartmentText(issue.cause_department) || '-'}</span></div>
<div><span class="font-medium text-blue-700">관리 코멘트:</span> <span class="text-blue-900">${cleanManagementComment(issue.management_comment) || '-'}</span></div>
</div>
</div>
<!-- 완료 정보 -->
<div class="bg-green-50 p-4 rounded-lg border border-green-200">
<h4 class="font-semibold text-green-800 mb-3 flex items-center">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
완료 정보
</h4>
<div class="space-y-3">
<!-- 완료 사진 -->
<div>
<label class="text-xs text-green-600 font-medium">완료 사진</label>
${(() => {
const photos = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<p class="text-xs text-gray-500 mt-1">완료 사진 없음</p>';
}
return `
<div class="mt-1 flex flex-wrap gap-2">
${photos.map(path => `
<img src="${path}" class="w-16 h-16 object-cover rounded-lg cursor-pointer border-2 border-green-200 hover:border-green-400 transition-colors" onclick="openPhotoModal('${path}')" alt="완료 사진">
`).join('')}
</div>
`;
})()}
</div>
<!-- 완료 코멘트 -->
<div>
<label class="text-xs text-green-600 font-medium">완료 코멘트</label>
<p class="text-xs text-green-800 mt-1 p-2 bg-white rounded border">${issue.completion_comment || '코멘트 없음'}</p>
</div>
<!-- 완료 신청일 -->
${issue.completion_requested_at ? `
<div>
<label class="text-xs text-green-600 font-medium">완료 신청일</label>
<p class="text-xs text-green-800 mt-1">${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}</p>
</div>
` : ''}
</div>
</div>
</div>
<!-- 하단 사진 영역 -->
<div class="mt-4 pt-4 border-t border-gray-200">
<h4 class="font-semibold text-gray-800 mb-2 flex items-center">
<i class="fas fa-camera text-gray-500 mr-2"></i>
업로드 사진
</h4>
${(() => {
const photos = [
issue.photo_path,
issue.photo_path2,
issue.photo_path3,
issue.photo_path4,
issue.photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>';
}
return `
<div class="flex flex-wrap gap-2">
${photos.map((path, idx) => `
<img src="${path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-gray-200" onclick="openPhotoModal('${path}')" alt="업로드 사진 ${idx + 1}">
`).join('')}
</div>
`;
})()}
</div>
</div>
`;
}
// 입력 여부 아이콘 생성
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() {
// 레거시 함수 - 더 이상 사용되지 않음
return '';
}
// 편집 가능한 필드 생성 함수
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 getDepartmentText(departmentCode) {
const departments = {
'production': '생산',
'quality': '품질',
'purchasing': '구매',
'design': '설계',
'sales': '영업'
};
return departments[departmentCode] || departmentCode;
}
// 날짜 그룹 토글 함수
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 = ['management_comment', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department'];
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>
${(() => {
const photos = [
issue.photo_path,
issue.photo_path2,
issue.photo_path3,
issue.photo_path4,
issue.photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<div class="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">없음</div>';
}
return `
<div class="flex flex-wrap gap-2">
${photos.map((path, idx) => `
<img src="${path}" class="w-20 h-20 object-cover rounded cursor-pointer border-2 border-gray-300 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${path}')" alt="업로드 사진 ${idx + 1}">
`).join('')}
</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_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" placeholder="확정된 해결 방안을 입력하세요...">${cleanManagementComment(issue.management_comment)}</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">${cleanManagementComment(issue.management_comment)}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">완료 사진 (최대 5장)</label>
<!-- 기존 완료 사진 표시 -->
${(() => {
const photos = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p);
if (photos.length > 0) {
return `
<div class="mb-3">
<p class="text-xs text-gray-600 mb-2">현재 완료 사진 (${photos.length}장)</p>
<div class="flex flex-wrap gap-2">
${photos.map(path => `
<img src="${path}" class="w-16 h-16 object-cover rounded cursor-pointer border-2 border-gray-300 hover:border-blue-400 transition-colors" onclick="openPhotoModal('${path}')" alt="완료 사진">
`).join('')}
</div>
</div>
`;
}
return '';
})()}
<!-- 사진 업로드 (최대 5장) -->
<div class="space-y-2">
<input type="file" id="modal_completion_photo" accept="image/*" multiple class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
<p class="text-xs text-gray-500">※ 최대 5장까지 업로드 가능합니다. 새로운 사진을 업로드하면 기존 사진을 모두 교체합니다.</p>
</div>
</div>
</div>
</div>
</div>
`;
}
async function saveModalChanges() {
if (!currentModalIssueId) return;
try {
// 편집된 필드들의 값 수집
const updates = {};
const fields = ['management_comment', 'responsible_department', 'responsible_person', 'expected_completion_date', 'cause_department'];
fields.forEach(field => {
const element = document.getElementById(`modal_${field}`);
if (element) {
let value = element.value.trim();
if (value === '' || value === '선택하세요') {
value = null;
}
updates[field] = value;
}
});
// 완료 사진 처리 (최대 5장)
const photoInput = document.getElementById('modal_completion_photo');
const photoFiles = photoInput.files;
if (photoFiles && photoFiles.length > 0) {
const maxPhotos = Math.min(photoFiles.length, 5);
for (let i = 0; i < maxPhotos; i++) {
const base64 = await fileToBase64(photoFiles[i]);
const fieldName = i === 0 ? 'completion_photo' : `completion_photo${i + 1}`;
updates[fieldName] = base64;
}
console.log(`📸 ${maxPhotos}장의 완료 사진 처리 완료`);
}
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);
}
}
// 추가 정보 폼 제출 처리 (요소가 존재할 때만)
const additionalInfoForm = document.getElementById('additionalInfoForm');
if (additionalInfoForm) {
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('저장 중 오류가 발생했습니다.');
}
});
}
// 상세 내용 편집 관련 함수들
function toggleDetailEdit(issueId) {
const displayDiv = document.getElementById(`detail-display-${issueId}`);
const editDiv = document.getElementById(`detail-edit-${issueId}`);
if (displayDiv && editDiv) {
displayDiv.classList.add('hidden');
editDiv.classList.remove('hidden');
// 텍스트 영역에 포커스
const textarea = document.getElementById(`detail-textarea-${issueId}`);
if (textarea) {
textarea.focus();
}
}
}
function cancelDetailEdit(issueId) {
const displayDiv = document.getElementById(`detail-display-${issueId}`);
const editDiv = document.getElementById(`detail-edit-${issueId}`);
if (displayDiv && editDiv) {
displayDiv.classList.remove('hidden');
editDiv.classList.add('hidden');
// 원래 값으로 복원
const issue = issues.find(i => i.id === issueId);
if (issue) {
const textarea = document.getElementById(`detail-textarea-${issueId}`);
if (textarea) {
textarea.value = getIssueDetail(issue);
}
}
}
}
async function saveDetailEdit(issueId) {
const textarea = document.getElementById(`detail-textarea-${issueId}`);
if (!textarea) return;
const newDetailContent = textarea.value.trim();
try {
// 현재 이슈 정보 가져오기
const issue = issues.find(i => i.id === issueId);
if (!issue) {
alert('이슈 정보를 찾을 수 없습니다.');
return;
}
// 부적합명과 새로운 상세 내용을 결합
const issueTitle = getIssueTitle(issue);
const combinedDescription = issueTitle + (newDetailContent ? '\n' + newDetailContent : '');
const response = await fetch(`/api/management/${issueId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
final_description: combinedDescription
})
});
if (response.ok) {
// 성공 시 이슈 데이터 업데이트
issue.final_description = combinedDescription;
// 표시 영역 업데이트
const displayDiv = document.getElementById(`detail-display-${issueId}`);
if (displayDiv) {
const contentDiv = displayDiv.querySelector('div');
if (contentDiv) {
contentDiv.textContent = newDetailContent || '상세 내용 없음';
}
}
// 편집 모드 종료
cancelDetailEdit(issueId);
alert('상세 내용이 성공적으로 저장되었습니다.');
} else {
const error = await response.json();
alert(`저장 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('상세 내용 저장 실패:', error);
alert('저장 중 오류가 발생했습니다.');
}
}
// 완료 확인 모달 열기 (진행 중 -> 완료 처리용)
function openCompletionConfirmModal(issueId) {
openIssueEditModal(issueId, true); // 완료 처리 모드로 열기
}
// 이슈 수정 모달 열기 (모든 진행 중 상태에서 사용)
function openIssueEditModal(issueId, isCompletionMode = false) {
const issue = issues.find(i => i.id === issueId);
if (!issue) return;
const project = projects.find(p => p.id === issue.project_id);
const isPendingCompletion = issue.completion_requested_at;
// 모달 내용 생성
const modalContent = `
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" id="issueEditModal">
<div class="bg-white rounded-xl shadow-xl max-w-6xl 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-xl font-semibold text-gray-900">
<i class="fas fa-edit text-blue-500 mr-2"></i>
이슈 수정 - No.${issue.project_sequence_no || '-'}
</h3>
<button onclick="closeIssueEditModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 이슈 정보 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- 왼쪽: 기본 정보 -->
<div class="space-y-4">
<div class="bg-blue-50 p-4 rounded-lg">
<h4 class="font-semibold text-blue-800 mb-3">기본 정보</h4>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
<input type="text" value="${project ? project.project_name : '-'}" class="w-full px-3 py-2 bg-gray-100 border border-gray-300 rounded-lg text-sm" readonly>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">부적합명</label>
<input type="text" id="edit-issue-title-${issue.id}" value="${getIssueTitle(issue)}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">상세내용</label>
<textarea id="edit-issue-detail-${issue.id}" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none">${getIssueDetail(issue)}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">원인분류</label>
<select id="edit-category-${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 text-sm">
<option value="material_missing" ${(issue.final_category || issue.category) === 'material_missing' ? 'selected' : ''}>자재 누락</option>
<option value="process_error" ${(issue.final_category || issue.category) === 'process_error' ? 'selected' : ''}>공정 오류</option>
<option value="quality_defect" ${(issue.final_category || issue.category) === 'quality_defect' ? 'selected' : ''}>품질 결함</option>
<option value="design_issue" ${(issue.final_category || issue.category) === 'design_issue' ? 'selected' : ''}>설계 문제</option>
<option value="safety_violation" ${(issue.final_category || issue.category) === 'safety_violation' ? 'selected' : ''}>안전 위반</option>
<option value="etc" ${(issue.final_category || issue.category) === 'etc' ? 'selected' : ''}>기타</option>
</select>
</div>
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-800 mb-3">업로드 사진</h4>
${(() => {
const photos = [
issue.photo_path,
issue.photo_path2,
issue.photo_path3,
issue.photo_path4,
issue.photo_path5
].filter(p => p);
if (photos.length === 0) {
return '<div class="w-20 h-20 bg-gray-100 rounded-lg flex items-center justify-center text-gray-400 text-xs">사진 없음</div>';
}
return `
<div class="flex flex-wrap gap-2">
${photos.map((path, idx) => `
<img src="${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('${path}')" alt="업로드 사진 ${idx + 1}">
`).join('')}
</div>
`;
})()}
</div>
</div>
<!-- 오른쪽: 관리 정보 -->
<div class="space-y-4">
<div class="bg-green-50 p-4 rounded-lg">
<h4 class="font-semibold text-green-800 mb-3">관리 정보</h4>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">해결방안 (확정)</label>
<textarea id="edit-management-comment-${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm resize-none" placeholder="확정된 해결 방안을 입력하세요...">${cleanManagementComment(issue.management_comment)}</textarea>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">담당부서</label>
<select id="edit-department-${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm">
${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="edit-person-${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm" placeholder="담당자 이름" value="${issue.responsible_person || ''}">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">조치예상일</label>
<input type="date" id="edit-date-${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm" 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="edit-cause-department-${issue.id}" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm">
${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="edit-management-comment-${issue.id}" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-sm resize-none" placeholder="관리 코멘트를 입력하세요...">${cleanManagementComment(issue.management_comment)}</textarea>
</div>
</div>
</div>
<!-- 완료 신청 정보 (진행 중, 완료 대기 둘 다 표시) -->
<div class="bg-purple-50 p-4 rounded-lg">
<h4 class="font-semibold text-purple-800 mb-3">완료 신청 정보</h4>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">완료 사진 (최대 5장)</label>
<div class="space-y-3">
${(() => {
const photos = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p);
if (photos.length > 0) {
return `
<div class="mb-3">
<p class="text-xs text-gray-600 mb-2">현재 완료 사진 (${photos.length}장)</p>
<div class="flex flex-wrap gap-2">
${photos.map(path => `
<img src="${path}" class="w-20 h-20 object-cover rounded-lg cursor-pointer border-2 border-purple-200 hover:border-purple-400 transition-colors" onclick="openPhotoModal('${path}')" alt="완료 사진">
`).join('')}
</div>
</div>
`;
} else {
return `
<div class="flex items-center justify-center w-24 h-24 bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg mb-3">
<div class="text-center">
<i class="fas fa-camera text-gray-400 text-lg mb-1"></i>
<p class="text-xs text-gray-500">사진 없음</p>
</div>
</div>
`;
}
})()}
<div class="flex items-center space-x-2">
<input type="file" id="edit-completion-photo-${issue.id}" accept="image/*" multiple class="hidden">
<button type="button" onclick="document.getElementById('edit-completion-photo-${issue.id}').click()" class="flex items-center px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors text-sm">
<i class="fas fa-upload mr-2"></i>
${(() => {
const photoCount = [
issue.completion_photo_path,
issue.completion_photo_path2,
issue.completion_photo_path3,
issue.completion_photo_path4,
issue.completion_photo_path5
].filter(p => p).length;
return photoCount > 0 ? '사진 교체' : '사진 업로드';
})()}
</button>
<span id="photo-filename-${issue.id}" class="text-sm text-gray-600"></span>
</div>
<p class="text-xs text-gray-500">※ 최대 5장까지 업로드 가능합니다. 새로운 사진을 업로드하면 기존 사진을 모두 교체합니다.</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">완료 코멘트</label>
<textarea id="edit-completion-comment-${issue.id}" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm resize-none" placeholder="완료 코멘트를 입력하세요...">${issue.completion_comment || ''}</textarea>
</div>
${isPendingCompletion ? `
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">신청일시</label>
<p class="text-sm text-gray-700">${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}</p>
</div>
` : ''}
</div>
</div>
</div>
</div>
<!-- 버튼 -->
<div class="flex justify-end space-x-3">
<button onclick="closeIssueEditModal()" class="px-6 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
취소
</button>
<button onclick="saveIssueFromModal(${issue.id})" class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<i class="fas fa-save mr-2"></i>저장
</button>
<button onclick="confirmDeleteIssue(${issue.id})" class="px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-trash mr-2"></i>삭제
</button>
<button onclick="saveAndCompleteIssue(${issue.id})" class="px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-check-circle mr-2"></i>최종확인
</button>
</div>
</div>
</div>
</div>
`;
// 모달을 body에 추가
document.body.insertAdjacentHTML('beforeend', modalContent);
// 파일 선택 이벤트 리스너 추가
const fileInput = document.getElementById(`edit-completion-photo-${issue.id}`);
const filenameSpan = document.getElementById(`photo-filename-${issue.id}`);
if (fileInput && filenameSpan) {
fileInput.addEventListener('change', function(e) {
if (e.target.files && e.target.files.length > 0) {
const fileCount = Math.min(e.target.files.length, 5);
filenameSpan.textContent = `${fileCount}개 파일 선택됨`;
filenameSpan.className = 'text-sm text-green-600 font-medium';
} else {
filenameSpan.textContent = '';
filenameSpan.className = 'text-sm text-gray-600';
}
});
}
}
// 이슈 수정 모달 닫기
function closeIssueEditModal() {
const modal = document.getElementById('issueEditModal');
if (modal) {
modal.remove();
}
}
// 파일을 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);
});
}
// 모달에서 이슈 저장
async function saveIssueFromModal(issueId) {
const title = document.getElementById(`edit-issue-title-${issueId}`).value.trim();
const detail = document.getElementById(`edit-issue-detail-${issueId}`).value.trim();
const category = document.getElementById(`edit-category-${issueId}`).value;
const managementComment = document.getElementById(`edit-management-comment-${issueId}`).value.trim();
const department = document.getElementById(`edit-department-${issueId}`).value;
const person = document.getElementById(`edit-person-${issueId}`).value.trim();
const date = document.getElementById(`edit-date-${issueId}`).value;
const causeDepartment = document.getElementById(`edit-cause-department-${issueId}`).value;
// 완료 신청 정보 (완료 대기 상태일 때만)
const completionCommentElement = document.getElementById(`edit-completion-comment-${issueId}`);
const completionPhotoElement = document.getElementById(`edit-completion-photo-${issueId}`);
let completionComment = null;
const completionPhotos = {}; // 완료 사진들을 저장할 객체
if (completionCommentElement) {
completionComment = completionCommentElement.value.trim();
}
// 완료 사진 처리 (최대 5장)
if (completionPhotoElement && completionPhotoElement.files.length > 0) {
try {
const files = completionPhotoElement.files;
const maxPhotos = Math.min(files.length, 5);
console.log(`🔍 총 ${maxPhotos}개의 완료 사진 업로드 시작`);
for (let i = 0; i < maxPhotos; i++) {
const file = files[i];
console.log(`🔍 파일 ${i + 1} 정보:`, {
name: file.name,
size: file.size,
type: file.type
});
const base64 = await fileToBase64(file);
const base64Data = base64.split(',')[1]; // Base64 데이터만 추출
const fieldName = i === 0 ? 'completion_photo' : `completion_photo${i + 1}`;
completionPhotos[fieldName] = base64Data;
console.log(`✅ 파일 ${i + 1} 변환 완료 (${fieldName})`);
}
} catch (error) {
console.error('파일 변환 오류:', error);
alert('완료 사진 업로드 중 오류가 발생했습니다.');
return;
}
}
if (!title) {
alert('부적합명을 입력해주세요.');
return;
}
const combinedDescription = title + (detail ? '\n' + detail : '');
const requestBody = {
final_description: combinedDescription,
final_category: category,
management_comment: managementComment || null,
responsible_department: department || null,
responsible_person: person || null,
expected_completion_date: date || null,
cause_department: causeDepartment || null
};
// 완료 신청 정보가 있으면 추가
if (completionComment !== null) {
requestBody.completion_comment = completionComment || null;
}
// 완료 사진들 추가 (최대 5장)
for (const [key, value] of Object.entries(completionPhotos)) {
requestBody[key] = value;
}
try {
const response = await fetch(`/api/management/${issueId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
// 저장 성공 후 데이터 새로고침하고 모달은 유지
await initializeManagement(); // 페이지 새로고침
// 저장된 이슈 정보 다시 로드하여 모달 업데이트
const updatedIssue = issues.find(i => i.id === issueId);
if (updatedIssue) {
// 완료 사진이 저장되었는지 확인
if (updatedIssue.completion_photo_path) {
alert('✅ 완료 사진이 성공적으로 저장되었습니다!');
} else {
alert('⚠️ 저장은 완료되었지만 완료 사진 저장에 실패했습니다. 다시 시도해주세요.');
}
// 모달 내용 업데이트 (완료 사진 표시 갱신)
const photoContainer = document.querySelector(`#issueEditModal img[alt*="완료 사진"]`)?.parentElement;
if (photoContainer && updatedIssue.completion_photo_path) {
// HEIC 파일인지 확인
const isHeic = updatedIssue.completion_photo_path.toLowerCase().endsWith('.heic');
if (isHeic) {
// HEIC 파일은 다운로드 링크로 표시
photoContainer.innerHTML = `
<div class="flex items-center space-x-3">
<div class="w-24 h-24 bg-purple-100 rounded-lg flex items-center justify-center border-2 border-purple-200">
<i class="fas fa-image text-purple-500 text-2xl"></i>
</div>
<div class="flex-1">
<p class="text-sm text-gray-600 mb-1">완료 사진 (HEIC)</p>
<a href="${updatedIssue.completion_photo_path}" download class="text-xs text-blue-500 hover:text-blue-700 underline">다운로드하여 확인</a>
</div>
</div>
`;
} else {
// 일반 이미지는 미리보기 표시
photoContainer.innerHTML = `
<div class="flex items-center space-x-3">
<img src="${updatedIssue.completion_photo_path}" class="w-24 h-24 object-cover rounded-lg cursor-pointer border-2 border-purple-200 hover:border-purple-400 transition-colors" onclick="openPhotoModal('${updatedIssue.completion_photo_path}')" alt="현재 완료 사진">
<div class="flex-1">
<p class="text-sm text-gray-600 mb-1">현재 완료 사진</p>
<p class="text-xs text-gray-500">클릭하면 크게 볼 수 있습니다</p>
</div>
</div>
`;
}
}
} else {
alert('이슈가 성공적으로 저장되었습니다.');
closeIssueEditModal();
}
} else {
const error = await response.json();
alert(`저장 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('저장 오류:', error);
alert('저장 중 오류가 발생했습니다.');
}
}
// 완료 대기 상태 관련 함수들
function editIssue(issueId) {
// 수정 모드로 전환 (완료 대기 상태를 해제)
if (confirm('완료 대기 상태를 해제하고 수정 모드로 전환하시겠습니까?')) {
// 완료 신청 정보 초기화 API 호출
resetCompletionRequest(issueId);
}
}
function rejectCompletion(issueId) {
const reason = prompt('반려 사유를 입력하세요:');
if (reason && reason.trim()) {
// 반려 처리 API 호출
rejectCompletionRequest(issueId, reason.trim());
}
}
function confirmCompletion(issueId) {
// 완료 확인 모달 열기 (수정 가능) - 통합 모달 사용
openIssueEditModal(issueId, true);
}
// 완료 신청 초기화 (수정 모드로 전환)
async function resetCompletionRequest(issueId) {
try {
const response = await fetch(`/api/issues/${issueId}/reset-completion`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
alert('완료 대기 상태가 해제되었습니다. 수정이 가능합니다.');
initializeManagement(); // 페이지 새로고침
} else {
const error = await response.json();
alert(`상태 변경 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('상태 변경 오류:', error);
alert('상태 변경 중 오류가 발생했습니다.');
}
}
// 완료 신청 반려
async function rejectCompletionRequest(issueId, reason) {
try {
const response = await fetch(`/api/issues/${issueId}/reject-completion`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
rejection_reason: reason
})
});
if (response.ok) {
alert('완료 신청이 반려되었습니다.');
initializeManagement(); // 페이지 새로고침
} else {
const error = await response.json();
alert(`반려 처리 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('반려 처리 오류:', error);
alert('반려 처리 중 오류가 발생했습니다.');
}
}
// 완료 확인 모달 열기
function openCompletionConfirmModal(issueId) {
const issue = issues.find(i => i.id === issueId);
if (!issue) return;
const project = projects.find(p => p.id === issue.project_id);
// 모달 내용 생성
const modalContent = `
<div class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center" id="completionConfirmModal">
<div class="bg-white rounded-xl shadow-xl max-w-4xl 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-xl font-semibold text-gray-900">
<i class="fas fa-check-circle text-green-500 mr-2"></i>
완료 확인 - No.${issue.project_sequence_no || '-'}
</h3>
<button onclick="closeCompletionConfirmModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- 이슈 정보 -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- 기본 정보 -->
<div class="space-y-4">
<div class="bg-blue-50 p-4 rounded-lg">
<h4 class="font-semibold text-blue-800 mb-2">기본 정보</h4>
<div class="space-y-2 text-sm">
<div><span class="font-medium">프로젝트:</span> ${project ? project.project_name : '-'}</div>
<div><span class="font-medium">부적합명:</span> ${getIssueTitle(issue)}</div>
<div><span class="font-medium">상세내용:</span> ${getIssueDetail(issue)}</div>
<div><span class="font-medium">원인분류:</span> ${getCategoryText(issue.final_category || issue.category)}</div>
</div>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<h4 class="font-semibold text-green-800 mb-2">관리 정보</h4>
<div class="space-y-2 text-sm">
<div><span class="font-medium">해결방안 (확정):</span> ${cleanManagementComment(issue.management_comment) || '-'}</div>
<div><span class="font-medium">담당부서:</span> ${issue.responsible_department || '-'}</div>
<div><span class="font-medium">담당자:</span> ${issue.responsible_person || '-'}</div>
<div><span class="font-medium">조치예상일:</span> ${issue.expected_completion_date ? new Date(issue.expected_completion_date).toLocaleDateString('ko-KR') : '-'}</div>
</div>
</div>
</div>
<!-- 완료 정보 -->
<div class="space-y-4">
<div class="bg-purple-50 p-4 rounded-lg">
<h4 class="font-semibold text-purple-800 mb-2">완료 신청 정보</h4>
<div class="space-y-3">
<div>
<span class="font-medium text-sm">완료 사진:</span>
${issue.completion_photo_path ? `
<div class="mt-2">
<img src="${issue.completion_photo_path}" class="w-32 h-32 object-cover rounded-lg border-2 border-purple-200 cursor-pointer" onclick="openPhotoModal('${issue.completion_photo_path}')" alt="완료 사진">
</div>
` : '<p class="text-sm text-gray-500 mt-1">완료 사진 없음</p>'}
</div>
<div>
<span class="font-medium text-sm">완료 코멘트:</span>
<p class="text-sm text-gray-700 mt-1 p-2 bg-white rounded border">${issue.completion_comment || '코멘트 없음'}</p>
</div>
<div>
<span class="font-medium text-sm">신청일시:</span>
<p class="text-sm text-gray-700 mt-1">${new Date(issue.completion_requested_at).toLocaleString('ko-KR')}</p>
</div>
</div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-800 mb-2">업로드 사진</h4>
<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" 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">사진 없음</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" 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">사진 없음</div>'}
</div>
</div>
</div>
</div>
<!-- 버튼 -->
<div class="flex justify-end space-x-3">
<button onclick="closeCompletionConfirmModal()" class="px-6 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors">
취소
</button>
<button onclick="finalConfirmCompletion(${issueId})" class="px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">
<i class="fas fa-check-circle mr-2"></i>최종 확인
</button>
</div>
</div>
</div>
</div>
`;
// 모달을 body에 추가
document.body.insertAdjacentHTML('beforeend', modalContent);
}
// 완료 확인 모달 닫기
function closeCompletionConfirmModal() {
const modal = document.getElementById('completionConfirmModal');
if (modal) {
modal.remove();
}
}
// 저장 후 완료 처리 (최종확인)
async function saveAndCompleteIssue(issueId) {
if (!confirm('수정 내용을 저장하고 이 부적합을 최종 완료 처리하시겠습니까?\n완료 처리 후에는 수정할 수 없습니다.')) {
return;
}
const title = document.getElementById(`edit-issue-title-${issueId}`).value.trim();
const detail = document.getElementById(`edit-issue-detail-${issueId}`).value.trim();
const category = document.getElementById(`edit-category-${issueId}`).value;
const managementComment = document.getElementById(`edit-management-comment-${issueId}`).value.trim();
const department = document.getElementById(`edit-department-${issueId}`).value;
const person = document.getElementById(`edit-person-${issueId}`).value.trim();
const date = document.getElementById(`edit-date-${issueId}`).value;
const causeDepartment = document.getElementById(`edit-cause-department-${issueId}`).value;
// 완료 신청 정보 (완료 대기 상태일 때만)
const completionCommentElement = document.getElementById(`edit-completion-comment-${issueId}`);
const completionPhotoElement = document.getElementById(`edit-completion-photo-${issueId}`);
let completionComment = null;
let completionPhoto = null;
if (completionCommentElement) {
completionComment = completionCommentElement.value.trim();
}
if (completionPhotoElement && completionPhotoElement.files[0]) {
try {
const file = completionPhotoElement.files[0];
console.log('🔍 업로드할 파일 정보:', {
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
});
const base64 = await fileToBase64(file);
console.log('🔍 Base64 변환 완료 - 전체 길이:', base64.length);
console.log('🔍 Base64 헤더:', base64.substring(0, 50));
completionPhoto = base64.split(',')[1]; // Base64 데이터만 추출
console.log('🔍 헤더 제거 후 길이:', completionPhoto.length);
console.log('🔍 전송할 Base64 시작 부분:', completionPhoto.substring(0, 50));
} catch (error) {
console.error('파일 변환 오류:', error);
alert('완료 사진 업로드 중 오류가 발생했습니다.');
return;
}
}
if (!title) {
alert('부적합명을 입력해주세요.');
return;
}
const combinedDescription = title + (detail ? '\n' + detail : '');
const requestBody = {
final_description: combinedDescription,
final_category: category,
management_comment: managementComment || null,
responsible_department: department || null,
responsible_person: person || null,
expected_completion_date: date || null,
cause_department: causeDepartment || null,
review_status: 'completed' // 완료 상태로 변경
};
// 완료 신청 정보가 있으면 추가
if (completionComment !== null) {
requestBody.completion_comment = completionComment || null;
}
if (completionPhoto !== null) {
requestBody.completion_photo = completionPhoto;
}
try {
// 1. 먼저 수정 내용 저장
const saveResponse = await fetch(`/api/management/${issueId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (saveResponse.ok) {
alert('부적합이 수정되고 최종 완료 처리되었습니다.');
closeIssueEditModal();
initializeManagement(); // 페이지 새로고침
} else {
const error = await saveResponse.json();
alert(`저장 및 완료 처리 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('저장 및 완료 처리 오류:', error);
alert('저장 및 완료 처리 중 오류가 발생했습니다.');
}
}
// 최종 완료 확인 (기존 함수 - 필요시 사용)
async function finalConfirmCompletion(issueId) {
if (!confirm('이 부적합을 최종 완료 처리하시겠습니까?\n완료 처리 후에는 수정할 수 없습니다.')) {
return;
}
try {
const response = await fetch(`/api/issues/${issueId}/final-completion`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
alert('부적합이 최종 완료 처리되었습니다.');
closeIssueEditModal();
initializeManagement(); // 페이지 새로고침
} else {
const error = await response.json();
alert(`완료 처리 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('완료 처리 오류:', error);
alert('완료 처리 중 오류가 발생했습니다.');
}
}
// 삭제 확인 다이얼로그
function confirmDeleteIssue(issueId) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[60]';
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
modal.innerHTML = `
<div class="bg-white rounded-lg p-6 w-96 max-w-md mx-4">
<div class="text-center mb-4">
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4">
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
</div>
<h3 class="text-lg font-semibold mb-2">부적합 삭제</h3>
<p class="text-sm text-gray-600">
이 부적합 사항을 삭제하시겠습니까?<br>
<strong class="text-red-600">삭제된 데이터는 로그로 보관되지만 복구할 수 없습니다.</strong>
</p>
</div>
<div class="flex gap-2">
<button onclick="this.closest('.fixed').remove()"
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
취소
</button>
<button onclick="handleDeleteIssueFromManagement(${issueId})"
class="flex-1 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
<i class="fas fa-trash mr-1"></i>삭제
</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
// 삭제 처리 함수
async function handleDeleteIssueFromManagement(issueId) {
try {
const response = await fetch(`/api/issues/${issueId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
alert('부적합이 삭제되었습니다.\n삭제 로그가 기록되었습니다.');
// 모달들 닫기
const deleteModal = document.querySelector('.fixed');
if (deleteModal) deleteModal.remove();
closeIssueEditModal();
// 페이지 새로고침
initializeManagement();
} else {
const error = await response.json();
alert(`삭제 실패: ${error.detail || '알 수 없는 오류'}`);
}
} catch (error) {
console.error('삭제 오류:', error);
alert('삭제 중 오류가 발생했습니다: ' + error.message);
}
}
</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>