- 신고 및 완료 사진 5장 지원 (photo_path3, photo_path4, photo_path5 추가) - 엑셀 일일 리포트 개선: - 사진 5장 모두 한 행에 일렬 배치 (A, C, E, G, I 열) - 상태별 색상 구분 (지연중: 빨강, 진행중: 노랑, 완료: 진한 초록) - 우선순위 기반 정렬 (지연중 → 진행중 → 완료됨) - 프로젝트 현황 통계 박스 UI 개선 (색상 구분) - 프론트엔드 모든 페이지 5장 사진 표시 (flex-wrap 레이아웃) - 관리함, 수신함, 현황판, 신고내용 확인 페이지 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1120 lines
51 KiB
HTML
1120 lines
51 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">
|
|
|
|
|
|
<!-- Custom Styles -->
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
|
|
body {
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 50%, #f0f9ff 100%);
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.glass-effect {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
}
|
|
|
|
.input-field {
|
|
background: white;
|
|
border: 1px solid #e5e7eb;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.input-field:focus {
|
|
outline: none;
|
|
border-color: #60a5fa;
|
|
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
|
|
}
|
|
|
|
.line-clamp-2 {
|
|
overflow: hidden;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.nav-link {
|
|
color: #6b7280;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.5rem;
|
|
transition: all 0.2s;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.nav-link:hover {
|
|
background-color: #f3f4f6;
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.nav-link.active {
|
|
background-color: #3b82f6;
|
|
color: white;
|
|
}
|
|
|
|
/* 부드러운 페이드인 애니메이션 */
|
|
.fade-in {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
|
}
|
|
|
|
.fade-in.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
/* 헤더 전용 빠른 페이드인 */
|
|
.header-fade-in {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
|
|
}
|
|
|
|
.header-fade-in.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
/* 본문 컨텐츠 지연 페이드인 */
|
|
.content-fade-in {
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
|
|
transition-delay: 0.2s;
|
|
}
|
|
|
|
.content-fade-in.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
|
|
|
|
<!-- Main Content -->
|
|
<main class="container mx-auto px-4 py-8 content-fade-in" style="padding-top: 80px;">
|
|
<!-- 페이지 헤더 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-4 mb-6">
|
|
<div class="mb-4">
|
|
<h1 id="pageTitle" class="text-xl font-bold text-gray-900 flex items-center">
|
|
<i class="fas fa-list-alt text-blue-500 mr-3"></i>
|
|
내 부적합 조회
|
|
</h1>
|
|
<p id="pageDescription" class="text-sm text-gray-600 mt-1">
|
|
내가 등록한 부적합 사항을 확인할 수 있습니다
|
|
</p>
|
|
</div>
|
|
|
|
<!-- 필터 섹션 -->
|
|
<div class="space-y-4 mb-6">
|
|
<!-- 기본 필터들 -->
|
|
<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">📁 프로젝트</label>
|
|
<select id="projectFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" onchange="filterIssues()">
|
|
<option value="">전체 프로젝트</option>
|
|
<!-- 프로젝트 옵션들이 여기에 로드됩니다 -->
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 워크플로우 상태 필터 -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">📋 워크플로우 상태</label>
|
|
<select id="reviewStatusFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" onchange="filterIssues()">
|
|
<option value="">전체</option>
|
|
<option value="pending_review">수신함 (검토 대기)</option>
|
|
<option value="in_progress">관리함 (진행 중)</option>
|
|
<option value="completed">관리함 (완료됨)</option>
|
|
<option value="disposed">폐기함 (폐기됨)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 날짜 필터 (시작/끝 날짜) -->
|
|
<div>
|
|
<label class="text-sm font-medium text-gray-700 mb-3 block">📅 기간 선택</label>
|
|
|
|
<!-- 빠른 선택 버튼 -->
|
|
<div class="flex flex-wrap gap-2 mb-3">
|
|
<button onclick="setDateRange('today')" class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors">오늘</button>
|
|
<button onclick="setDateRange('week')" class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors">이번 주</button>
|
|
<button onclick="setDateRange('month')" class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors">이번 달</button>
|
|
<button onclick="setDateRange('all')" class="px-3 py-1 text-xs bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 transition-colors">전체</button>
|
|
</div>
|
|
|
|
<!-- 날짜 입력 필드 -->
|
|
<div class="space-y-2">
|
|
<div>
|
|
<label class="text-xs text-gray-600 mb-1 block">시작날짜:</label>
|
|
<input type="date" id="startDateInput" class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-gray-600 mb-1 block">끝날짜:</label>
|
|
<input type="date" id="endDateInput" class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
|
</div>
|
|
<button onclick="applyDateFilter()" class="w-full px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
|
<i class="fas fa-search mr-2"></i>조회
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 결과 섹션 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-6">
|
|
<div id="issueResults" class="space-y-3">
|
|
<!-- 결과가 여기에 표시됩니다 -->
|
|
<div class="text-gray-500 text-center py-8">
|
|
<i class="fas fa-spinner fa-spin text-3xl mb-3"></i>
|
|
<p>데이터를 불러오는 중...</p>
|
|
</div>
|
|
</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 currentRange = 'week'; // 기본값: 이번 주
|
|
|
|
// 애니메이션 함수들
|
|
function animateHeaderAppearance() {
|
|
console.log('🎨 헤더 애니메이션 시작');
|
|
|
|
// 헤더 요소 찾기 (공통 헤더가 생성한 요소)
|
|
const headerElement = document.querySelector('header') || document.querySelector('[class*="header"]') || document.querySelector('nav');
|
|
|
|
if (headerElement) {
|
|
headerElement.classList.add('header-fade-in');
|
|
setTimeout(() => {
|
|
headerElement.classList.add('visible');
|
|
console.log('✨ 헤더 페이드인 완료');
|
|
|
|
// 헤더 애니메이션 완료 후 본문 애니메이션
|
|
setTimeout(() => {
|
|
animateContentAppearance();
|
|
}, 200);
|
|
}, 50);
|
|
} else {
|
|
// 헤더를 찾지 못했으면 바로 본문 애니메이션
|
|
console.log('⚠️ 헤더 요소를 찾지 못함 - 본문 애니메이션 시작');
|
|
animateContentAppearance();
|
|
}
|
|
}
|
|
|
|
// 본문 컨텐츠 애니메이션
|
|
function animateContentAppearance() {
|
|
console.log('🎨 본문 컨텐츠 애니메이션 시작');
|
|
|
|
// 모든 content-fade-in 요소들을 순차적으로 애니메이션
|
|
const contentElements = document.querySelectorAll('.content-fade-in');
|
|
|
|
contentElements.forEach((element, index) => {
|
|
setTimeout(() => {
|
|
element.classList.add('visible');
|
|
console.log(`✨ 컨텐츠 ${index + 1} 페이드인 완료`);
|
|
}, index * 100); // 100ms씩 지연
|
|
});
|
|
}
|
|
|
|
// API 로드 후 초기화 함수
|
|
async function initializeIssueView() {
|
|
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_view');
|
|
|
|
// 헤더 초기화 후 부드러운 애니메이션 시작
|
|
setTimeout(() => {
|
|
animateHeaderAppearance();
|
|
}, 100);
|
|
|
|
// 사용자 역할에 따른 페이지 제목 설정
|
|
updatePageTitle(user);
|
|
|
|
// 페이지 접근 권한 체크 (부적합 조회 페이지)
|
|
setTimeout(() => {
|
|
if (!canAccessPage('issues_view')) {
|
|
alert('부적합 조회 페이지에 접근할 권한이 없습니다.');
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
}, 500);
|
|
|
|
} catch (error) {
|
|
console.error('인증 실패:', error);
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('currentUser');
|
|
window.location.href = '/index.html';
|
|
return;
|
|
}
|
|
|
|
// 네비게이션은 공통 헤더에서 처리됨
|
|
|
|
// 프로젝트 로드
|
|
await loadProjects();
|
|
|
|
// 기본 날짜 설정 (이번 주)
|
|
setDefaultDateRange();
|
|
|
|
// 기본값: 이번 주 데이터 로드
|
|
await loadIssues();
|
|
setDateRange('week');
|
|
}
|
|
|
|
// 이미지 모달 표시
|
|
function showImageModal(imagePath) {
|
|
if (!imagePath) return;
|
|
|
|
// 모달 HTML 생성
|
|
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="relative max-w-4xl max-h-[90vh]">
|
|
<img src="${imagePath}" class="max-w-full max-h-[90vh] object-contain rounded-lg">
|
|
<button
|
|
onclick="this.parentElement.parentElement.remove()"
|
|
class="absolute top-2 right-2 p-2 bg-red-500 text-white rounded-full hover:bg-red-600"
|
|
>
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
// 네비게이션은 공통 헤더에서 처리됨
|
|
|
|
// 기본 날짜 범위 설정
|
|
function setDefaultDateRange() {
|
|
const today = new Date();
|
|
const weekStart = new Date(today);
|
|
weekStart.setDate(today.getDate() - today.getDay()); // 이번 주 일요일
|
|
|
|
// 날짜 입력 필드에 기본값 설정
|
|
document.getElementById('startDateInput').value = formatDateForInput(weekStart);
|
|
document.getElementById('endDateInput').value = formatDateForInput(today);
|
|
}
|
|
|
|
// 날짜를 input[type="date"] 형식으로 포맷
|
|
function formatDateForInput(date) {
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
|
|
// 날짜 필터 적용
|
|
function applyDateFilter() {
|
|
const startDate = document.getElementById('startDateInput').value;
|
|
const endDate = document.getElementById('endDateInput').value;
|
|
|
|
if (!startDate || !endDate) {
|
|
alert('시작날짜와 끝날짜를 모두 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
if (new Date(startDate) > new Date(endDate)) {
|
|
alert('시작날짜는 끝날짜보다 이전이어야 합니다.');
|
|
return;
|
|
}
|
|
|
|
// 필터 적용
|
|
filterIssues();
|
|
}
|
|
|
|
// 사용자 역할에 따른 페이지 제목 업데이트
|
|
function updatePageTitle(user) {
|
|
const titleElement = document.getElementById('pageTitle');
|
|
const descriptionElement = document.getElementById('pageDescription');
|
|
|
|
if (user.role === 'admin') {
|
|
titleElement.innerHTML = `
|
|
<i class="fas fa-list-alt text-blue-500 mr-3"></i>
|
|
전체 부적합 조회
|
|
`;
|
|
descriptionElement.textContent = '모든 사용자가 등록한 부적합 사항을 관리할 수 있습니다';
|
|
} else {
|
|
titleElement.innerHTML = `
|
|
<i class="fas fa-list-alt text-blue-500 mr-3"></i>
|
|
내 부적합 조회
|
|
`;
|
|
descriptionElement.textContent = '내가 등록한 부적합 사항을 확인할 수 있습니다';
|
|
}
|
|
}
|
|
|
|
// 프로젝트 로드 (API 기반)
|
|
async function loadProjects() {
|
|
try {
|
|
// 모든 프로젝트 로드 (활성/비활성 모두 - 기존 데이터 조회를 위해)
|
|
projects = await ProjectsAPI.getAll(false);
|
|
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.job_no} / ${project.project_name}${!project.is_active ? ' (비활성)' : ''}`;
|
|
projectFilter.appendChild(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('프로젝트 로드 실패:', error);
|
|
}
|
|
}
|
|
|
|
// 이슈 필터링
|
|
// 검토 상태 확인 함수
|
|
function isReviewCompleted(issue) {
|
|
return issue.status === 'complete' && issue.work_hours && issue.work_hours > 0;
|
|
}
|
|
|
|
// 날짜 필터링 함수
|
|
function filterByDate(issues, dateFilter) {
|
|
const now = new Date();
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
switch (dateFilter) {
|
|
case 'today':
|
|
return issues.filter(issue => {
|
|
const issueDate = new Date(issue.report_date);
|
|
return issueDate >= today;
|
|
});
|
|
case 'week':
|
|
const weekStart = new Date(today);
|
|
weekStart.setDate(today.getDate() - today.getDay());
|
|
return issues.filter(issue => {
|
|
const issueDate = new Date(issue.report_date);
|
|
return issueDate >= weekStart;
|
|
});
|
|
case 'month':
|
|
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
return issues.filter(issue => {
|
|
const issueDate = new Date(issue.report_date);
|
|
return issueDate >= monthStart;
|
|
});
|
|
default:
|
|
return issues;
|
|
}
|
|
}
|
|
|
|
// 날짜 범위별 필터링 함수
|
|
function filterByDateRange(issues, range) {
|
|
const now = new Date();
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
switch (range) {
|
|
case 'today':
|
|
return issues.filter(issue => {
|
|
const issueDate = new Date(issue.created_at);
|
|
const issueDay = new Date(issueDate.getFullYear(), issueDate.getMonth(), issueDate.getDate());
|
|
return issueDay.getTime() === today.getTime();
|
|
});
|
|
|
|
case 'week':
|
|
const weekStart = new Date(today);
|
|
weekStart.setDate(today.getDate() - today.getDay());
|
|
const weekEnd = new Date(weekStart);
|
|
weekEnd.setDate(weekStart.getDate() + 6);
|
|
weekEnd.setHours(23, 59, 59, 999);
|
|
|
|
return issues.filter(issue => {
|
|
const issueDate = new Date(issue.created_at);
|
|
return issueDate >= weekStart && issueDate <= weekEnd;
|
|
});
|
|
|
|
case 'month':
|
|
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
|
monthEnd.setHours(23, 59, 59, 999);
|
|
|
|
return issues.filter(issue => {
|
|
const issueDate = new Date(issue.created_at);
|
|
return issueDate >= monthStart && issueDate <= monthEnd;
|
|
});
|
|
|
|
default:
|
|
return issues;
|
|
}
|
|
}
|
|
|
|
function filterIssues() {
|
|
// 필터 값 가져오기
|
|
const selectedProjectId = document.getElementById('projectFilter').value;
|
|
const reviewStatusFilter = document.getElementById('reviewStatusFilter').value;
|
|
|
|
let filteredIssues = [...issues];
|
|
|
|
// 프로젝트 필터 적용
|
|
if (selectedProjectId) {
|
|
filteredIssues = filteredIssues.filter(issue => {
|
|
const issueProjectId = issue.project_id || issue.projectId;
|
|
return issueProjectId && (issueProjectId == selectedProjectId || issueProjectId.toString() === selectedProjectId.toString());
|
|
});
|
|
}
|
|
|
|
// 워크플로우 상태 필터 적용
|
|
if (reviewStatusFilter) {
|
|
filteredIssues = filteredIssues.filter(issue => {
|
|
// 새로운 워크플로우 시스템 사용
|
|
if (issue.review_status) {
|
|
return issue.review_status === reviewStatusFilter;
|
|
}
|
|
// 기존 데이터 호환성을 위한 폴백
|
|
else {
|
|
const isCompleted = isReviewCompleted(issue);
|
|
if (reviewStatusFilter === 'pending_review') return !isCompleted;
|
|
if (reviewStatusFilter === 'completed') return isCompleted;
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 날짜 범위 필터 적용 (입력 필드에서 선택된 범위)
|
|
const startDateInput = document.getElementById('startDateInput').value;
|
|
const endDateInput = document.getElementById('endDateInput').value;
|
|
|
|
if (startDateInput && endDateInput) {
|
|
filteredIssues = filteredIssues.filter(issue => {
|
|
const issueDate = new Date(issue.report_date);
|
|
const startOfDay = new Date(startDateInput);
|
|
startOfDay.setHours(0, 0, 0, 0);
|
|
const endOfDay = new Date(endDateInput);
|
|
endOfDay.setHours(23, 59, 59, 999);
|
|
|
|
return issueDate >= startOfDay && issueDate <= endOfDay;
|
|
});
|
|
}
|
|
|
|
// 전역 변수에 필터링된 결과 저장
|
|
window.filteredIssues = filteredIssues;
|
|
|
|
displayResults();
|
|
}
|
|
|
|
// 프로젝트 정보 표시용 함수
|
|
function getProjectInfo(projectId) {
|
|
if (!projectId) {
|
|
return '<span class="text-gray-500">프로젝트 미지정</span>';
|
|
}
|
|
|
|
// 전역 projects 배열에서 찾기
|
|
const project = projects.find(p => p.id == projectId);
|
|
if (project) {
|
|
return `${project.job_no} / ${project.project_name}`;
|
|
}
|
|
|
|
return `<span class="text-red-500">프로젝트 ID: ${projectId} (정보 없음)</span>`;
|
|
}
|
|
|
|
// 날짜 범위 설정 및 자동 조회
|
|
function setDateRange(range) {
|
|
currentRange = range;
|
|
|
|
const today = new Date();
|
|
let startDate, endDate;
|
|
|
|
switch (range) {
|
|
case 'today':
|
|
startDate = new Date(today);
|
|
endDate = new Date(today);
|
|
break;
|
|
case 'week':
|
|
startDate = new Date(today);
|
|
startDate.setDate(today.getDate() - today.getDay()); // 이번 주 일요일
|
|
endDate = new Date(today);
|
|
break;
|
|
case 'month':
|
|
startDate = new Date(today.getFullYear(), today.getMonth(), 1); // 이번 달 1일
|
|
endDate = new Date(today);
|
|
break;
|
|
case 'all':
|
|
startDate = new Date(2020, 0, 1); // 충분히 과거 날짜
|
|
endDate = new Date(today);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// 날짜 입력 필드 업데이트
|
|
document.getElementById('startDateInput').value = formatDateForInput(startDate);
|
|
document.getElementById('endDateInput').value = formatDateForInput(endDate);
|
|
|
|
// 필터 적용
|
|
filterIssues();
|
|
}
|
|
|
|
// 부적합 사항 로드 (자신이 올린 내용만)
|
|
async function loadIssues() {
|
|
const container = document.getElementById('issueResults');
|
|
container.innerHTML = `
|
|
<div class="text-gray-500 text-center py-8">
|
|
<i class="fas fa-spinner fa-spin text-3xl mb-3"></i>
|
|
<p>데이터를 불러오는 중...</p>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
// 모든 이슈 가져오기
|
|
const allIssues = await IssuesAPI.getAll();
|
|
|
|
// 자신이 올린 이슈만 필터링
|
|
issues = allIssues
|
|
.filter(issue => issue.reporter_id === currentUser.id)
|
|
.sort((a, b) => new Date(b.report_date) - new Date(a.report_date));
|
|
|
|
// 결과 표시
|
|
filterIssues();
|
|
|
|
} catch (error) {
|
|
console.error('부적합 사항 로드 실패:', error);
|
|
container.innerHTML = `
|
|
<div class="text-red-500 text-center py-8">
|
|
<i class="fas fa-exclamation-triangle text-3xl mb-3"></i>
|
|
<p>데이터를 불러오는데 실패했습니다.</p>
|
|
<button onclick="loadIssues()" class="mt-3 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
|
|
다시 시도
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// 결과 표시 (시간순 나열)
|
|
function displayResults() {
|
|
const container = document.getElementById('issueResults');
|
|
|
|
// 필터링된 결과 사용 (filterIssues에서 설정됨)
|
|
const filteredIssues = window.filteredIssues || issues;
|
|
|
|
if (filteredIssues.length === 0) {
|
|
const emptyMessage = currentUser.role === 'admin'
|
|
? '조건에 맞는 부적합 사항이 없습니다.'
|
|
: '아직 등록한 부적합 사항이 없습니다.<br><small class="text-sm">부적합 등록 페이지에서 새로운 부적합을 등록해보세요.</small>';
|
|
|
|
container.innerHTML = `
|
|
<div class="text-gray-500 text-center py-12">
|
|
<i class="fas fa-inbox text-4xl mb-4 text-gray-400"></i>
|
|
<p class="text-lg mb-2">${emptyMessage}</p>
|
|
${currentUser.role !== 'admin' ? `
|
|
<div class="mt-4">
|
|
<a href="/index.html" class="inline-flex items-center px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
|
|
<i class="fas fa-plus mr-2"></i>
|
|
부적합 등록하기
|
|
</a>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// 워크플로우 상태별로 분류 및 정렬
|
|
const groupedIssues = {
|
|
pending_review: filteredIssues.filter(issue =>
|
|
issue.review_status === 'pending_review' || (!issue.review_status && !isReviewCompleted(issue))
|
|
),
|
|
in_progress: filteredIssues.filter(issue => issue.review_status === 'in_progress'),
|
|
completed: filteredIssues.filter(issue =>
|
|
issue.review_status === 'completed' || (!issue.review_status && isReviewCompleted(issue))
|
|
),
|
|
disposed: filteredIssues.filter(issue => issue.review_status === 'disposed')
|
|
};
|
|
|
|
container.innerHTML = '';
|
|
|
|
// 각 상태별로 표시
|
|
const statusConfig = [
|
|
{ key: 'pending_review', title: '수신함 (검토 대기)', icon: 'fas fa-inbox', color: 'text-orange-700' },
|
|
{ key: 'in_progress', title: '관리함 (진행 중)', icon: 'fas fa-cog', color: 'text-blue-700' },
|
|
{ key: 'completed', title: '관리함 (완료됨)', icon: 'fas fa-check-circle', color: 'text-green-700' },
|
|
{ key: 'disposed', title: '폐기함 (폐기됨)', icon: 'fas fa-trash', color: 'text-gray-700' }
|
|
];
|
|
|
|
statusConfig.forEach((config, index) => {
|
|
const issues = groupedIssues[config.key];
|
|
if (issues.length > 0) {
|
|
const header = document.createElement('div');
|
|
header.className = index > 0 ? 'mb-4 mt-8' : 'mb-4';
|
|
header.innerHTML = `
|
|
<h3 class="text-md font-semibold ${config.color} flex items-center">
|
|
<i class="${config.icon} mr-2"></i>${config.title} (${issues.length}건)
|
|
</h3>
|
|
`;
|
|
container.appendChild(header);
|
|
|
|
issues.forEach(issue => {
|
|
container.appendChild(createIssueCard(issue, config.key === 'completed'));
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// 워크플로우 상태 표시 함수
|
|
function getWorkflowStatusBadge(issue) {
|
|
const status = issue.review_status || (isReviewCompleted(issue) ? 'completed' : 'pending_review');
|
|
|
|
const statusConfig = {
|
|
'pending_review': { text: '검토 대기', class: 'bg-orange-100 text-orange-700', icon: 'fas fa-inbox' },
|
|
'in_progress': { text: '진행 중', class: 'bg-blue-100 text-blue-700', icon: 'fas fa-cog' },
|
|
'completed': { text: '완료됨', class: 'bg-green-100 text-green-700', icon: 'fas fa-check-circle' },
|
|
'disposed': { text: '폐기됨', class: 'bg-gray-100 text-gray-700', icon: 'fas fa-trash' }
|
|
};
|
|
|
|
const config = statusConfig[status] || statusConfig['pending_review'];
|
|
return `<span class="px-2 py-1 rounded-full text-xs font-medium ${config.class}">
|
|
<i class="${config.icon} mr-1"></i>${config.text}
|
|
</span>`;
|
|
}
|
|
|
|
// 부적합 사항 카드 생성 함수 (조회용)
|
|
function createIssueCard(issue, isCompleted) {
|
|
const categoryNames = {
|
|
material_missing: '자재누락',
|
|
design_error: '설계미스',
|
|
incoming_defect: '입고자재 불량',
|
|
inspection_miss: '검사미스'
|
|
};
|
|
|
|
const categoryColors = {
|
|
material_missing: 'bg-yellow-100 text-yellow-700 border-yellow-300',
|
|
design_error: 'bg-blue-100 text-blue-700 border-blue-300',
|
|
incoming_defect: 'bg-red-100 text-red-700 border-red-300',
|
|
inspection_miss: 'bg-purple-100 text-purple-700 border-purple-300'
|
|
};
|
|
|
|
const div = document.createElement('div');
|
|
// 검토 완료 상태에 따른 스타일링
|
|
const baseClasses = 'rounded-lg transition-colors border-l-4 mb-4';
|
|
const statusClasses = isCompleted
|
|
? 'bg-gray-100 opacity-75'
|
|
: 'bg-gray-50 hover:bg-gray-100';
|
|
const borderColor = categoryColors[issue.category]?.split(' ')[2] || 'border-gray-300';
|
|
div.className = `${baseClasses} ${statusClasses} ${borderColor}`;
|
|
|
|
const dateStr = DateUtils.formatKST(issue.report_date, true);
|
|
const relativeTime = DateUtils.getRelativeTime(issue.report_date);
|
|
const projectInfo = getProjectInfo(issue.project_id || issue.projectId);
|
|
|
|
// 수정/삭제 권한 확인 (본인이 등록한 부적합만)
|
|
const canEdit = issue.reporter_id === currentUser.id;
|
|
const canDelete = issue.reporter_id === currentUser.id || currentUser.role === 'admin';
|
|
|
|
div.innerHTML = `
|
|
<!-- 프로젝트 정보 및 상태 (오른쪽 상단) -->
|
|
<div class="flex justify-between items-start p-2 pb-0">
|
|
<div class="flex items-center gap-2">
|
|
${getWorkflowStatusBadge(issue)}
|
|
</div>
|
|
<div class="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
|
<i class="fas fa-folder-open mr-1"></i>${projectInfo}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 기존 내용 -->
|
|
<div class="flex gap-3 p-3 pt-1">
|
|
<!-- 사진들 -->
|
|
<div class="flex gap-1 flex-shrink-0 flex-wrap max-w-md">
|
|
${(() => {
|
|
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">
|
|
<i class="fas fa-image text-gray-400"></i>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return photos.map(path => `
|
|
<img src="${path}" class="w-20 h-20 object-cover rounded shadow-sm cursor-pointer" onclick="showImageModal('${path}')">
|
|
`).join('');
|
|
})()}
|
|
</div>
|
|
|
|
<!-- 내용 -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-start justify-between mb-2">
|
|
<span class="px-2 py-1 rounded-full text-xs font-medium ${categoryColors[issue.category] || 'bg-gray-100 text-gray-700'}">
|
|
${categoryNames[issue.category] || issue.category}
|
|
</span>
|
|
${issue.work_hours ?
|
|
`<span class="text-sm text-green-600 font-medium">
|
|
<i class="fas fa-clock mr-1"></i>${issue.work_hours}시간
|
|
</span>` :
|
|
'<span class="text-sm text-gray-400">시간 미입력</span>'
|
|
}
|
|
</div>
|
|
|
|
<p class="text-gray-800 mb-2 line-clamp-2">${issue.description}</p>
|
|
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-4 text-sm text-gray-500">
|
|
<span><i class="fas fa-user mr-1"></i>${issue.reporter?.full_name || issue.reporter?.username || '알 수 없음'}</span>
|
|
<span><i class="fas fa-calendar mr-1"></i>${dateStr}</span>
|
|
<span class="text-xs text-gray-400">${relativeTime}</span>
|
|
</div>
|
|
|
|
<!-- 수정/삭제 버튼 -->
|
|
${(canEdit || canDelete) ? `
|
|
<div class="flex gap-2">
|
|
${canEdit ? `
|
|
<button onclick='showEditModal(${JSON.stringify(issue).replace(/'/g, "'")})' class="px-3 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
|
|
<i class="fas fa-edit mr-1"></i>수정
|
|
</button>
|
|
` : ''}
|
|
${canDelete ? `
|
|
<button onclick="confirmDelete(${issue.id})" class="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600 transition-colors">
|
|
<i class="fas fa-trash mr-1"></i>삭제
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return div;
|
|
}
|
|
|
|
// 관리 버튼 클릭 처리
|
|
function handleAdminClick() {
|
|
if (currentUser.role === 'admin') {
|
|
// 관리자: 사용자 관리 페이지로 이동
|
|
window.location.href = 'admin.html';
|
|
}
|
|
}
|
|
|
|
// 비밀번호 변경 모달 표시
|
|
function showPasswordChangeModal() {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
|
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="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-semibold">비밀번호 변경</h3>
|
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="passwordChangeForm" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">현재 비밀번호</label>
|
|
<input type="password" id="currentPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
|
|
<input type="password" id="newPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required minlength="6">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label>
|
|
<input type="password" id="confirmPassword" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required>
|
|
</div>
|
|
|
|
<div class="flex gap-2 pt-4">
|
|
<button type="button" onclick="this.closest('.fixed').remove()"
|
|
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">
|
|
취소
|
|
</button>
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
|
변경
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// 폼 제출 이벤트 처리
|
|
document.getElementById('passwordChangeForm').addEventListener('submit', handlePasswordChange);
|
|
}
|
|
|
|
// 비밀번호 변경 처리
|
|
async function handlePasswordChange(e) {
|
|
e.preventDefault();
|
|
|
|
const currentPassword = document.getElementById('currentPassword').value;
|
|
const newPassword = document.getElementById('newPassword').value;
|
|
const confirmPassword = document.getElementById('confirmPassword').value;
|
|
|
|
// 새 비밀번호 확인
|
|
if (newPassword !== confirmPassword) {
|
|
alert('새 비밀번호가 일치하지 않습니다.');
|
|
return;
|
|
}
|
|
|
|
// 현재 비밀번호 확인 (localStorage 기반)
|
|
let users = JSON.parse(localStorage.getItem('work-report-users') || '[]');
|
|
|
|
// 기본 사용자가 없으면 생성
|
|
if (users.length === 0) {
|
|
users = [
|
|
{
|
|
username: 'hyungi',
|
|
full_name: '관리자',
|
|
password: 'djg3-jj34-X3Q3',
|
|
role: 'admin'
|
|
}
|
|
];
|
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
|
}
|
|
|
|
let user = users.find(u => u.username === currentUser.username);
|
|
|
|
// 사용자가 없으면 기본값으로 생성
|
|
if (!user) {
|
|
const username = currentUser.username;
|
|
user = {
|
|
username: username,
|
|
full_name: username === 'hyungi' ? '관리자' : username,
|
|
password: 'djg3-jj34-X3Q3',
|
|
role: username === 'hyungi' ? 'admin' : 'user'
|
|
};
|
|
users.push(user);
|
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
|
}
|
|
|
|
if (user.password !== currentPassword) {
|
|
alert('현재 비밀번호가 올바르지 않습니다.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 비밀번호 변경
|
|
user.password = newPassword;
|
|
localStorage.setItem('work-report-users', JSON.stringify(users));
|
|
|
|
// 현재 사용자 정보도 업데이트
|
|
currentUser.password = newPassword;
|
|
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
|
|
|
alert('비밀번호가 성공적으로 변경되었습니다.');
|
|
document.querySelector('.fixed').remove(); // 모달 닫기
|
|
|
|
} catch (error) {
|
|
alert('비밀번호 변경에 실패했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// 로그아웃 함수
|
|
function logout() {
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('currentUser');
|
|
window.location.href = 'index.html';
|
|
}
|
|
|
|
// 수정 모달 표시
|
|
function showEditModal(issue) {
|
|
const categoryNames = {
|
|
material_missing: '자재누락',
|
|
design_error: '설계미스',
|
|
incoming_defect: '입고자재 불량',
|
|
inspection_miss: '검사미스'
|
|
};
|
|
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
|
|
modal.onclick = (e) => {
|
|
if (e.target === modal) modal.remove();
|
|
};
|
|
|
|
modal.innerHTML = `
|
|
<div class="bg-white rounded-lg p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-semibold">부적합 수정</h3>
|
|
<button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="editIssueForm" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">카테고리</label>
|
|
<select id="editCategory" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required>
|
|
<option value="material_missing" ${issue.category === 'material_missing' ? 'selected' : ''}>자재누락</option>
|
|
<option value="design_error" ${issue.category === 'design_error' ? 'selected' : ''}>설계미스</option>
|
|
<option value="incoming_defect" ${issue.category === 'incoming_defect' ? 'selected' : ''}>입고자재 불량</option>
|
|
<option value="inspection_miss" ${issue.category === 'inspection_miss' ? 'selected' : ''}>검사미스</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label>
|
|
<select id="editProject" class="w-full px-3 py-2 border border-gray-300 rounded-lg" required>
|
|
${projects.map(p => `
|
|
<option value="${p.id}" ${p.id === issue.project_id ? 'selected' : ''}>
|
|
${p.job_no} / ${p.project_name}
|
|
</option>
|
|
`).join('')}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">내용</label>
|
|
<textarea id="editDescription" class="w-full px-3 py-2 border border-gray-300 rounded-lg" rows="4" required>${issue.description || ''}</textarea>
|
|
</div>
|
|
|
|
<div class="flex gap-2 pt-4">
|
|
<button type="button" onclick="this.closest('.fixed').remove()"
|
|
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50">
|
|
취소
|
|
</button>
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
|
수정
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// 폼 제출 이벤트 처리
|
|
document.getElementById('editIssueForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const updateData = {
|
|
category: document.getElementById('editCategory').value,
|
|
description: document.getElementById('editDescription').value,
|
|
project_id: parseInt(document.getElementById('editProject').value)
|
|
};
|
|
|
|
try {
|
|
await IssuesAPI.update(issue.id, updateData);
|
|
alert('수정되었습니다.');
|
|
modal.remove();
|
|
// 목록 새로고침
|
|
await loadIssues();
|
|
} catch (error) {
|
|
console.error('수정 실패:', error);
|
|
alert('수정에 실패했습니다: ' + error.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 삭제 확인 다이얼로그
|
|
function confirmDelete(issueId) {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
|
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>
|
|
삭제된 데이터는 로그로 보관되지만 복구할 수 없습니다.
|
|
</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">
|
|
취소
|
|
</button>
|
|
<button onclick="handleDelete(${issueId})"
|
|
class="flex-1 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600">
|
|
삭제
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
// 삭제 처리
|
|
async function handleDelete(issueId) {
|
|
try {
|
|
await IssuesAPI.delete(issueId);
|
|
alert('삭제되었습니다.');
|
|
|
|
// 모달 닫기
|
|
const modal = document.querySelector('.fixed');
|
|
if (modal) modal.remove();
|
|
|
|
// 목록 새로고침
|
|
await loadIssues();
|
|
} catch (error) {
|
|
console.error('삭제 실패:', error);
|
|
alert('삭제에 실패했습니다: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// 네비게이션은 공통 헤더에서 처리됨
|
|
|
|
// API 스크립트 동적 로딩
|
|
const cacheBuster = Date.now() + Math.random() + Math.floor(Math.random() * 1000000);
|
|
const script = document.createElement('script');
|
|
script.src = `/static/js/api.js?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 스크립트 로드 완료 (issue-view.html)');
|
|
// API 로드 후 초기화 시작
|
|
initializeIssueView();
|
|
};
|
|
script.onerror = function() {
|
|
console.error('❌ API 스크립트 로드 실패');
|
|
};
|
|
document.head.appendChild(script);
|
|
</script>
|
|
</body>
|
|
</html>
|