Files
M-Project/frontend/issue-view.html
Hyungi Ahn 610a171b25 feat: 모든 페이지에 공통 헤더 적용 및 모바일 최적화
- 모든 HTML 페이지에 권한 기반 공통 헤더 적용
- 부적합 등록 페이지 모바일 최적화 (사진 업로드 UI 개선)
- 부적합 조회 페이지에 모바일 캘린더 날짜 필터 적용
- 사용자별 권한에 따른 동적 페이지 제목 및 메시지 표시

Page Updates:
- index.html: 모바일 친화적 사진 업로드 UI, 공통 헤더 적용
- issue-view.html: 터치/스와이프 캘린더 필터, 권한별 조회 제한
- daily-work.html: 공통 헤더 적용, 프로젝트 로딩 로직 개선
- project-management.html: 공통 헤더 적용, 권한 체크 강화
- admin.html: 페이지 권한 관리 UI 추가, 공통 헤더 적용

Mobile Optimizations:
- 터치 타겟 최소 44px 보장
- 스와이프 제스처 지원
- 반응형 레이아웃
- 모바일 전용 UI 컴포넌트
2025-10-25 09:01:32 +09:00

870 lines
40 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', -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;
}
</style>
</head>
<body>
<!-- 공통 헤더가 여기에 자동으로 삽입됩니다 -->
<!-- Main Content -->
<main class="container mx-auto px-4 py-8">
<!-- 페이지 헤더 -->
<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">검토 필요</option>
<option value="completed">검토 완료</option>
</select>
</div>
</div>
<!-- 날짜 필터 (캘린더) -->
<div>
<div class="flex items-center justify-between mb-3">
<label class="text-sm font-medium text-gray-700">📅 기간 선택</label>
<button id="toggleCalendar" class="text-sm text-blue-600 hover:text-blue-800 flex items-center">
<span id="calendarToggleText">캘린더 열기</span>
<i class="fas fa-chevron-down ml-1" id="calendarToggleIcon"></i>
</button>
</div>
<!-- 캘린더 컨테이너 -->
<div id="calendarContainer" class="bg-white border border-gray-200 rounded-xl p-4" style="display: none;">
<!-- 모바일 캘린더가 여기에 렌더링됩니다 -->
</div>
<!-- 선택된 날짜 범위 표시 -->
<div id="currentDateRange" class="mt-2 text-sm text-gray-600 bg-gray-50 px-3 py-2 rounded-lg">
<i class="fas fa-calendar-alt mr-2"></i>
<span id="dateRangeText">이번 주</span>
</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 src="/static/js/components/mobile-calendar.js?v=20251025"></script>
<script>
let currentUser = null;
let issues = [];
let projects = []; // 프로젝트 데이터 캐시
let currentRange = 'week'; // 기본값: 이번 주
let mobileCalendar = null; // 모바일 캘린더 인스턴스
let selectedStartDate = null;
let selectedEndDate = null;
// 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');
// 사용자 역할에 따른 페이지 제목 설정
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();
// 캘린더 초기화
initializeMobileCalendar();
// 기본값: 이번 주 데이터 로드
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 initializeMobileCalendar() {
// 캘린더 토글 버튼 이벤트
document.getElementById('toggleCalendar').addEventListener('click', () => {
const container = document.getElementById('calendarContainer');
const toggleText = document.getElementById('calendarToggleText');
const toggleIcon = document.getElementById('calendarToggleIcon');
if (container.style.display === 'none') {
container.style.display = 'block';
toggleText.textContent = '캘린더 닫기';
toggleIcon.classList.remove('fa-chevron-down');
toggleIcon.classList.add('fa-chevron-up');
// 캘린더 인스턴스가 없으면 생성
if (!mobileCalendar) {
mobileCalendar = new MobileCalendar('calendarContainer', {
onRangeSelect: (startDate, endDate) => {
handleDateRangeSelect(startDate, endDate);
}
});
}
} else {
container.style.display = 'none';
toggleText.textContent = '캘린더 열기';
toggleIcon.classList.remove('fa-chevron-up');
toggleIcon.classList.add('fa-chevron-down');
}
});
}
// 날짜 범위 선택 처리
function handleDateRangeSelect(startDate, endDate) {
selectedStartDate = startDate;
selectedEndDate = endDate;
// 날짜 범위 텍스트 업데이트
updateDateRangeText(startDate, endDate);
// 필터 적용
filterIssues();
}
// 날짜 범위 텍스트 업데이트
function updateDateRangeText(startDate, endDate) {
const dateRangeText = document.getElementById('dateRangeText');
if (!startDate && !endDate) {
dateRangeText.textContent = '전체 기간';
currentRange = 'all';
} else if (startDate && endDate) {
const start = startDate.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
const end = endDate.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
const daysDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1;
dateRangeText.textContent = `${start} ~ ${end} (${daysDiff}일)`;
currentRange = 'custom';
} else if (startDate) {
const start = startDate.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
dateRangeText.textContent = `${start} (선택 중...)`;
currentRange = 'custom';
}
}
// 사용자 역할에 따른 페이지 제목 업데이트
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 => {
const isCompleted = isReviewCompleted(issue);
return reviewStatusFilter === 'completed' ? isCompleted : !isCompleted;
});
}
// 날짜 범위 필터 적용 (캘린더에서 선택된 범위)
if (selectedStartDate && selectedEndDate) {
filteredIssues = filteredIssues.filter(issue => {
const issueDate = new Date(issue.created_at);
const startOfDay = new Date(selectedStartDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(selectedEndDate);
endOfDay.setHours(23, 59, 59, 999);
return issueDate >= startOfDay && issueDate <= endOfDay;
});
} else if (currentRange && currentRange !== 'all' && currentRange !== 'custom') {
// 빠른 선택 범위 적용
filteredIssues = filterByDateRange(filteredIssues, currentRange);
}
// 전역 변수에 필터링된 결과 저장
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>`;
}
// 날짜 범위 설정 및 자동 조회
async function setDateRange(range) {
currentRange = range;
// 버튼 스타일 업데이트
document.querySelectorAll('button[onclick^="setDateRange"]').forEach(btn => {
if (btn.textContent.includes('전체') && range === 'all') {
btn.className = 'px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm';
} else if (btn.textContent.includes('오늘') && range === 'today') {
btn.className = 'px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm';
} else if (btn.textContent.includes('이번 주') && range === 'week') {
btn.className = 'px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm';
} else if (btn.textContent.includes('이번 달') && range === 'month') {
btn.className = 'px-3 py-1.5 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm';
} else {
btn.className = 'px-3 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors text-sm';
}
});
await loadIssues(range);
}
// 부적합 사항 로드
async function loadIssues(range) {
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();
// 날짜 필터링
const today = new Date();
today.setHours(23, 59, 59, 999);
let startDate = new Date();
switch(range) {
case 'today':
startDate = new Date();
startDate.setHours(0, 0, 0, 0);
break;
case 'week':
startDate.setDate(today.getDate() - 7);
startDate.setHours(0, 0, 0, 0);
break;
case 'month':
startDate.setMonth(today.getMonth() - 1);
startDate.setHours(0, 0, 0, 0);
break;
case 'all':
startDate = new Date(2020, 0, 1); // 충분히 과거 날짜
break;
}
// 필터링 및 정렬 (최신순)
issues = allIssues
.filter(issue => {
const issueDate = new Date(issue.report_date);
return issueDate >= startDate && issueDate <= today;
})
.sort((a, b) => new Date(b.report_date) - new Date(a.report_date));
// 결과 표시
displayResults();
} catch (error) {
console.error('조회 실패:', error);
container.innerHTML = `
<div class="text-red-500 text-center py-8">
<i class="fas fa-exclamation-circle text-3xl mb-3"></i>
<p>데이터를 불러오는데 실패했습니다.</p>
</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 pendingIssues = filteredIssues.filter(issue => !isReviewCompleted(issue));
const completedIssues = filteredIssues.filter(issue => isReviewCompleted(issue));
container.innerHTML = '';
// 검토 필요 항목을 먼저 표시
if (pendingIssues.length > 0) {
const pendingHeader = document.createElement('div');
pendingHeader.className = 'mb-4';
pendingHeader.innerHTML = `
<h3 class="text-md font-semibold text-orange-700 flex items-center">
<i class="fas fa-exclamation-triangle mr-2"></i>검토 필요 (${pendingIssues.length}건)
</h3>
`;
container.appendChild(pendingHeader);
pendingIssues.forEach(issue => {
container.appendChild(createIssueCard(issue, false));
});
}
// 검토 완료 항목을 아래에 표시
if (completedIssues.length > 0) {
const completedHeader = document.createElement('div');
completedHeader.className = 'mb-4 mt-8';
completedHeader.innerHTML = `
<h3 class="text-md font-semibold text-green-700 flex items-center">
<i class="fas fa-check-circle mr-2"></i>검토 완료 (${completedIssues.length}건)
</h3>
`;
container.appendChild(completedHeader);
completedIssues.forEach(issue => {
container.appendChild(createIssueCard(issue, true));
});
}
}
// 부적합 사항 카드 생성 함수 (조회용)
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);
div.innerHTML = `
<!-- 프로젝트 정보 및 상태 (오른쪽 상단) -->
<div class="flex justify-between items-start p-2 pb-0">
<div class="flex items-center gap-2">
${isCompleted ?
'<div class="px-2 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium"><i class="fas fa-check-circle mr-1"></i>검토완료</div>' :
'<div class="px-2 py-1 bg-orange-100 text-orange-800 rounded-full text-xs font-medium"><i class="fas fa-exclamation-triangle mr-1"></i>검토필요</div>'
}
</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">
${issue.photo_path ?
`<img src="${issue.photo_path}" class="w-20 h-20 object-cover rounded shadow-sm cursor-pointer" onclick="showImageModal('${issue.photo_path}')">` : ''
}
${issue.photo_path2 ?
`<img src="${issue.photo_path2}" class="w-20 h-20 object-cover rounded shadow-sm cursor-pointer" onclick="showImageModal('${issue.photo_path2}')">` : ''
}
${!issue.photo_path && !issue.photo_path2 ?
`<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>` : ''
}
</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 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>
</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';
}
// 네비게이션은 공통 헤더에서 처리됨
// 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>