- Frontend: 하드코딩된 localhost API URL을 동적 URL 생성으로 변경 - reports-daily.html: 3곳 수정 (프로젝트 로드, 미리보기, 보고서 생성) - issues-archive.html: 프로젝트 로드 함수 수정 - issues-dashboard.html: 2곳 수정 (프로젝트 로드, 진행중 이슈 로드) - issues-inbox.html: 프로젝트 로드 함수 수정 - daily-work.html: 프로젝트 로드 함수 수정 - permissions.js: 2곳 수정 (권한 부여, 사용자 권한 조회) - Backup System: 완전한 백업/복구 시스템 구축 - backup_script.sh: 자동 백업 스크립트 (DB, 볼륨, 설정 파일) - restore_script.sh: 백업 복구 스크립트 - setup_auto_backup.sh: 자동 백업 스케줄 설정 (매일 오후 9시) - 백업 정책: 최신 10개 버전만 유지하여 용량 절약 - Migration: 5장 사진 지원 마이그레이션 파일 업데이트 이제 Cloudflare 환경(m.hyungi.net)에서 HTTPS 프로토콜로 API 호출하여 Mixed Content 오류 없이 모든 기능이 정상 작동합니다.
620 lines
27 KiB
HTML
620 lines
27 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;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.issue-card:hover {
|
|
opacity: 1;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.archived-card {
|
|
border-left: 4px solid #6b7280;
|
|
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
|
|
}
|
|
|
|
.completed-card {
|
|
border-left: 4px solid #10b981;
|
|
background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 100%);
|
|
}
|
|
|
|
.badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge-completed { background: #d1fae5; color: #065f46; }
|
|
.badge-archived { background: #f3f4f6; color: #374151; }
|
|
.badge-cancelled { background: #fee2e2; color: #991b1b; }
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
height: 300px;
|
|
}
|
|
</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-archive text-gray-500 mr-3"></i>
|
|
폐기함
|
|
</h1>
|
|
<p class="text-gray-600 mt-1">완료되거나 폐기된 부적합 사항을 보관하고 분석하세요</p>
|
|
</div>
|
|
<div class="flex items-center space-x-3">
|
|
<button onclick="generateReport()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
|
<i class="fas fa-chart-bar mr-2"></i>
|
|
통계 보고서
|
|
</button>
|
|
<button onclick="cleanupArchive()" class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">
|
|
<i class="fas fa-trash mr-2"></i>
|
|
정리하기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 아카이브 통계 -->
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<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 class="bg-gray-50 p-4 rounded-lg">
|
|
<div class="flex items-center">
|
|
<i class="fas fa-archive 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="archivedCount">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-red-50 p-4 rounded-lg">
|
|
<div class="flex items-center">
|
|
<i class="fas fa-times-circle text-red-500 text-xl mr-3"></i>
|
|
<div>
|
|
<p class="text-sm text-red-600">취소</p>
|
|
<p class="text-2xl font-bold text-red-700" id="cancelledCount">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-purple-50 p-4 rounded-lg">
|
|
<div class="flex items-center">
|
|
<i class="fas fa-calendar-alt 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="thisMonthCount">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 필터 및 검색 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-6 mb-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|
<!-- 프로젝트 필터 -->
|
|
<div>
|
|
<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-gray-500 focus:border-gray-500" onchange="filterIssues()">
|
|
<option value="">전체 프로젝트</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 상태 필터 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">📋 상태</label>
|
|
<select id="statusFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-500 focus:border-gray-500" onchange="filterIssues()">
|
|
<option value="">전체</option>
|
|
<option value="completed">완료</option>
|
|
<option value="archived">보관</option>
|
|
<option value="cancelled">취소</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 기간 필터 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">📅 기간</label>
|
|
<select id="periodFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-500 focus:border-gray-500" onchange="filterIssues()">
|
|
<option value="">전체 기간</option>
|
|
<option value="week">이번 주</option>
|
|
<option value="month">이번 달</option>
|
|
<option value="quarter">이번 분기</option>
|
|
<option value="year">올해</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 카테고리 필터 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">🏷️ 카테고리</label>
|
|
<select id="categoryFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-500 focus:border-gray-500" onchange="filterIssues()">
|
|
<option value="">전체 카테고리</option>
|
|
<option value="material_missing">자재 누락</option>
|
|
<option value="design_error">설계 오류</option>
|
|
<option value="incoming_defect">반입 불량</option>
|
|
<option value="inspection_miss">검사 누락</option>
|
|
<option value="etc">기타</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 검색 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">🔍 검색</label>
|
|
<input type="text" id="searchInput" placeholder="설명 또는 등록자 검색..."
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-gray-500 focus:border-gray-500"
|
|
onkeyup="filterIssues()">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 통계 차트 -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<!-- 월별 완료 현황 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-4">월별 완료 현황</h3>
|
|
<div class="chart-container">
|
|
<canvas id="monthlyChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 카테고리별 분포 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
<h3 class="text-lg font-semibold text-gray-800 mb-4">카테고리별 분포</h3>
|
|
<div class="chart-container">
|
|
<canvas id="categoryChart"></canvas>
|
|
</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-2">
|
|
<span class="text-sm text-gray-500">정렬:</span>
|
|
<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>
|
|
<option value="completed">완료일순</option>
|
|
<option value="category">카테고리순</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="issuesList" class="divide-y divide-gray-200">
|
|
<!-- 부적합 목록이 여기에 동적으로 생성됩니다 -->
|
|
</div>
|
|
|
|
<!-- 빈 상태 -->
|
|
<div id="emptyState" class="hidden p-12 text-center">
|
|
<i class="fas fa-archive 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>
|
|
|
|
<!-- 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 = [];
|
|
|
|
// API 로드 후 초기화 함수
|
|
async function initializeArchive() {
|
|
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_archive');
|
|
|
|
// 페이지 접근 권한 체크
|
|
setTimeout(() => {
|
|
if (!canAccessPage('issues_archive')) {
|
|
alert('폐기함 페이지에 접근할 권한이 없습니다.');
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
}, 500);
|
|
|
|
// 데이터 로드
|
|
await loadProjects();
|
|
await loadArchivedIssues();
|
|
|
|
} catch (error) {
|
|
console.error('인증 실패:', error);
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('currentUser');
|
|
window.location.href = '/index.html';
|
|
}
|
|
}
|
|
|
|
// 프로젝트 로드
|
|
async function loadProjects() {
|
|
try {
|
|
const apiUrl = window.API_BASE_URL || (() => {
|
|
const hostname = window.location.hostname;
|
|
const protocol = window.location.protocol;
|
|
const port = window.location.port;
|
|
|
|
if ((hostname === 'localhost' || hostname === '127.0.0.1') && port) {
|
|
return `${protocol}//${hostname}:${port}/api`;
|
|
}
|
|
if (hostname === 'm.hyungi.net') {
|
|
return 'https://m-api.hyungi.net/api';
|
|
}
|
|
return '/api';
|
|
})();
|
|
const response = await fetch(`${apiUrl}/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 loadArchivedIssues() {
|
|
try {
|
|
let endpoint = '/api/issues/';
|
|
|
|
// 관리자인 경우 전체 부적합 조회 API 사용
|
|
if (currentUser.role === 'admin') {
|
|
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();
|
|
// 폐기된 부적합만 필터링 (폐기함 전용)
|
|
issues = allIssues.filter(issue =>
|
|
issue.review_status === 'disposed'
|
|
);
|
|
|
|
filterIssues();
|
|
updateStatistics();
|
|
renderCharts();
|
|
} else {
|
|
throw new Error('부적합 목록을 불러올 수 없습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('부적합 로드 실패:', error);
|
|
alert('부적합 목록을 불러오는데 실패했습니다.');
|
|
}
|
|
}
|
|
|
|
// 필터링 및 표시
|
|
function filterIssues() {
|
|
const projectFilter = document.getElementById('projectFilter').value;
|
|
const statusFilter = document.getElementById('statusFilter').value;
|
|
const periodFilter = document.getElementById('periodFilter').value;
|
|
const categoryFilter = document.getElementById('categoryFilter').value;
|
|
const searchInput = document.getElementById('searchInput').value.toLowerCase();
|
|
|
|
filteredIssues = issues.filter(issue => {
|
|
if (projectFilter && issue.project_id != projectFilter) return false;
|
|
if (statusFilter && issue.status !== statusFilter) return false;
|
|
if (categoryFilter && issue.category !== categoryFilter) return false;
|
|
|
|
// 기간 필터
|
|
if (periodFilter) {
|
|
const issueDate = new Date(issue.updated_at || issue.created_at);
|
|
const now = new Date();
|
|
|
|
switch (periodFilter) {
|
|
case 'week':
|
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
if (issueDate < weekAgo) return false;
|
|
break;
|
|
case 'month':
|
|
const monthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
|
|
if (issueDate < monthAgo) return false;
|
|
break;
|
|
case 'quarter':
|
|
const quarterAgo = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
|
|
if (issueDate < quarterAgo) return false;
|
|
break;
|
|
case 'year':
|
|
const yearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
|
|
if (issueDate < yearAgo) return false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (searchInput) {
|
|
const searchText = `${issue.description} ${issue.reporter?.username || ''}`.toLowerCase();
|
|
if (!searchText.includes(searchInput)) return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
sortIssues();
|
|
displayIssues();
|
|
}
|
|
|
|
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);
|
|
case 'completed':
|
|
return new Date(b.disposed_at || b.report_date) - new Date(a.disposed_at || a.report_date);
|
|
case 'category':
|
|
return (a.category || '').localeCompare(b.category || '');
|
|
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');
|
|
|
|
container.innerHTML = filteredIssues.map(issue => {
|
|
const project = projects.find(p => p.id === issue.project_id);
|
|
|
|
// 폐기함은 폐기된 것만 표시
|
|
const completedDate = issue.disposed_at ? new Date(issue.disposed_at).toLocaleDateString('ko-KR') : 'Invalid Date';
|
|
const statusText = '폐기';
|
|
const cardClass = 'archived-card';
|
|
|
|
return `
|
|
<div class="issue-card p-6 ${cardClass} cursor-pointer"
|
|
onclick="viewArchivedIssue(${issue.id})">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center space-x-3 mb-2">
|
|
<span class="badge badge-${getStatusBadgeClass(issue.status)}">${getStatusText(issue.status)}</span>
|
|
${project ? `<span class="text-sm text-gray-500">${project.project_name}</span>` : ''}
|
|
<span class="text-sm text-gray-400">${completedDate}</span>
|
|
</div>
|
|
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">${issue.description}</h3>
|
|
|
|
<div class="flex items-center text-sm text-gray-500 space-x-4">
|
|
<span><i class="fas fa-user mr-1"></i>${issue.reporter?.username || '알 수 없음'}</span>
|
|
${issue.category ? `<span><i class="fas fa-tag mr-1"></i>${getCategoryText(issue.category)}</span>` : ''}
|
|
<span><i class="fas fa-clock mr-1"></i>${statusText}: ${completedDate}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-2 ml-4">
|
|
<i class="fas fa-${getStatusIcon(issue.status)} text-2xl ${getStatusColor(issue.status)}"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// 통계 업데이트
|
|
function updateStatistics() {
|
|
const completed = issues.filter(issue => issue.status === 'completed').length;
|
|
const archived = issues.filter(issue => issue.status === 'archived').length;
|
|
const cancelled = issues.filter(issue => issue.status === 'cancelled').length;
|
|
|
|
const thisMonth = issues.filter(issue => {
|
|
const issueDate = new Date(issue.updated_at || issue.created_at);
|
|
const now = new Date();
|
|
return issueDate.getMonth() === now.getMonth() && issueDate.getFullYear() === now.getFullYear();
|
|
}).length;
|
|
|
|
document.getElementById('completedCount').textContent = completed;
|
|
document.getElementById('archivedCount').textContent = archived;
|
|
document.getElementById('cancelledCount').textContent = cancelled;
|
|
document.getElementById('thisMonthCount').textContent = thisMonth;
|
|
}
|
|
|
|
// 차트 렌더링 (간단한 텍스트 기반)
|
|
function renderCharts() {
|
|
renderMonthlyChart();
|
|
renderCategoryChart();
|
|
}
|
|
|
|
function renderMonthlyChart() {
|
|
const canvas = document.getElementById('monthlyChart');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// 간단한 차트 대신 텍스트로 표시
|
|
canvas.width = canvas.offsetWidth;
|
|
canvas.height = canvas.offsetHeight;
|
|
|
|
ctx.fillStyle = '#374151';
|
|
ctx.font = '16px Inter';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('월별 완료 현황 차트', canvas.width / 2, canvas.height / 2);
|
|
ctx.font = '12px Inter';
|
|
ctx.fillText('(차트 라이브러리 구현 예정)', canvas.width / 2, canvas.height / 2 + 20);
|
|
}
|
|
|
|
function renderCategoryChart() {
|
|
const canvas = document.getElementById('categoryChart');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
canvas.width = canvas.offsetWidth;
|
|
canvas.height = canvas.offsetHeight;
|
|
|
|
ctx.fillStyle = '#374151';
|
|
ctx.font = '16px Inter';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('카테고리별 분포 차트', canvas.width / 2, canvas.height / 2);
|
|
ctx.font = '12px Inter';
|
|
ctx.fillText('(차트 라이브러리 구현 예정)', canvas.width / 2, canvas.height / 2 + 20);
|
|
}
|
|
|
|
// 기타 함수들
|
|
function generateReport() {
|
|
alert('통계 보고서를 생성합니다.');
|
|
}
|
|
|
|
function cleanupArchive() {
|
|
if (confirm('오래된 보관 데이터를 정리하시겠습니까?')) {
|
|
alert('데이터 정리가 완료되었습니다.');
|
|
}
|
|
}
|
|
|
|
function viewArchivedIssue(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 = {
|
|
'completed': 'completed',
|
|
'archived': 'archived',
|
|
'cancelled': 'cancelled'
|
|
};
|
|
return statusMap[status] || 'archived';
|
|
}
|
|
|
|
function getStatusText(status) {
|
|
const statusMap = {
|
|
'completed': '완료',
|
|
'archived': '보관',
|
|
'cancelled': '취소'
|
|
};
|
|
return statusMap[status] || status;
|
|
}
|
|
|
|
function getStatusIcon(status) {
|
|
const iconMap = {
|
|
'completed': 'check-circle',
|
|
'archived': 'archive',
|
|
'cancelled': 'times-circle'
|
|
};
|
|
return iconMap[status] || 'archive';
|
|
}
|
|
|
|
function getStatusColor(status) {
|
|
const colorMap = {
|
|
'completed': 'text-green-500',
|
|
'archived': 'text-gray-500',
|
|
'cancelled': 'text-red-500'
|
|
};
|
|
return colorMap[status] || 'text-gray-500';
|
|
}
|
|
|
|
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-archive.html)');
|
|
initializeArchive();
|
|
};
|
|
script.onerror = function() {
|
|
console.error('❌ API 스크립트 로드 실패');
|
|
};
|
|
document.head.appendChild(script);
|
|
</script>
|
|
</body>
|
|
</html>
|