feat: 프로젝트 관리 및 비밀번호 변경 기능 개선
주요 변경사항: - 비활성화된 프로젝트 관리 기능 추가 * 프로젝트 관리 페이지에 접을 수 있는 비활성 프로젝트 섹션 추가 * 비활성화된 프로젝트 복구 기능 제공 * 업로드 시에는 활성 프로젝트만 표시되도록 API 호출 분리 - 헤더 비밀번호 변경 기능 완전 구현 * CommonHeader.js에 완전한 비밀번호 변경 모달 구현 * ESC 키 지원, 실시간 유효성 검사, 토스트 메시지 추가 * 중복 코드 제거 및 통일된 함수 호출 구조 - 수신함 수정 내용 표시 문제 해결 * description 우선 표시로 최신 수정 내용 반영 * 관리함에서 final_description/final_category 업데이트 로직 추가 - 현황판 날짜 그룹화 개선 * 업로드일 기준에서 관리함 진입일(reviewed_at) 기준으로 변경 * Invalid Date 오류 해결 - 프로젝트 관리 페이지 JavaScript 오류 수정 * 중복 변수 선언 및 함수 참조 오류 해결 * 페이지 초기화 로직 개선 기술적 개선: - API 호출 최적화 (active_only 매개변수 명시적 전달) - 프론트엔드 표시 우선순위 통일 (description || final_description) - 백엔드 final_* 필드 업데이트 로직 추가
This commit is contained in:
@@ -183,7 +183,7 @@
|
||||
|
||||
<!-- 하단 메뉴 -->
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 border-t bg-gray-50">
|
||||
<button onclick="showPasswordChangeModal()" class="w-full text-left p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<button onclick="CommonHeader.showPasswordModal()" class="w-full text-left p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<i class="fas fa-key mr-3 text-gray-500"></i>
|
||||
<span class="text-gray-700">비밀번호 변경</span>
|
||||
</button>
|
||||
@@ -296,44 +296,6 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 비밀번호 변경 모달 -->
|
||||
<div id="passwordModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-xl 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="hidePasswordChangeModal()" 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="input-field w-full px-3 py-2 rounded-lg" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
|
||||
<input type="password" id="newPassword" class="input-field w-full px-3 py-2 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="input-field w-full px-3 py-2 rounded-lg" required>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-4">
|
||||
<button type="button" onclick="hidePasswordChangeModal()"
|
||||
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 btn-primary rounded-lg">
|
||||
변경
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/static/js/utils/date-utils.js"></script>
|
||||
|
||||
@@ -1192,8 +1192,8 @@
|
||||
console.log('=== 프로젝트 로드 시작 (API) ===');
|
||||
|
||||
try {
|
||||
// API에서 프로젝트 로드 (인증 없이)
|
||||
const apiProjects = await ProjectsAPI.getAll(false);
|
||||
// API에서 활성 프로젝트만 로드 (업로드용)
|
||||
const apiProjects = await ProjectsAPI.getAll(true);
|
||||
|
||||
// API 데이터를 UI 형식으로 변환
|
||||
const projects = apiProjects.map(p => ({
|
||||
|
||||
@@ -322,9 +322,9 @@
|
||||
|
||||
if (response.ok) {
|
||||
const allIssues = await response.json();
|
||||
// 완료, 보관, 취소된 부적합만 필터링
|
||||
// 폐기된 부적합만 필터링 (폐기함 전용)
|
||||
issues = allIssues.filter(issue =>
|
||||
['completed', 'archived', 'cancelled'].includes(issue.status)
|
||||
issue.review_status === 'disposed'
|
||||
);
|
||||
|
||||
filterIssues();
|
||||
@@ -422,8 +422,11 @@
|
||||
|
||||
container.innerHTML = filteredIssues.map(issue => {
|
||||
const project = projects.find(p => p.id === issue.project_id);
|
||||
const completedDate = new Date(issue.updated_at || issue.created_at).toLocaleDateString('ko-KR');
|
||||
const cardClass = issue.status === 'completed' ? 'completed-card' : 'archived-card';
|
||||
|
||||
// 폐기함은 폐기된 것만 표시
|
||||
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"
|
||||
@@ -441,7 +444,7 @@
|
||||
<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>완료: ${completedDate}</span>
|
||||
<span><i class="fas fa-clock mr-1"></i>${statusText}: ${completedDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -423,22 +423,29 @@
|
||||
|
||||
emptyState.classList.add('hidden');
|
||||
|
||||
// 날짜별로 그룹화
|
||||
// 날짜별로 그룹화 (관리함 진입일 기준)
|
||||
const groupedByDate = {};
|
||||
const dateObjects = {}; // 정렬용 Date 객체 저장
|
||||
|
||||
filteredIssues.forEach(issue => {
|
||||
const dateKey = new Date(issue.report_date).toLocaleDateString('ko-KR');
|
||||
// reviewed_at이 있으면 관리함 진입일, 없으면 report_date 사용
|
||||
const dateToUse = issue.reviewed_at || issue.report_date;
|
||||
const dateObj = new Date(dateToUse);
|
||||
const dateKey = dateObj.toLocaleDateString('ko-KR');
|
||||
|
||||
if (!groupedByDate[dateKey]) {
|
||||
groupedByDate[dateKey] = [];
|
||||
dateObjects[dateKey] = dateObj;
|
||||
}
|
||||
groupedByDate[dateKey].push(issue);
|
||||
});
|
||||
|
||||
// 날짜별 그룹 생성
|
||||
const dateGroups = Object.keys(groupedByDate)
|
||||
.sort((a, b) => new Date(b) - new Date(a)) // 최신순
|
||||
.sort((a, b) => dateObjects[b] - dateObjects[a]) // 최신순
|
||||
.map(dateKey => {
|
||||
const issues = groupedByDate[dateKey];
|
||||
const formattedDate = new Date(dateKey).toLocaleDateString('ko-KR', {
|
||||
const formattedDate = dateObjects[dateKey].toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
@@ -451,7 +458,7 @@
|
||||
<i class="fas fa-chevron-down transition-transform duration-200" id="chevron-${dateKey}"></i>
|
||||
<span class="font-semibold text-gray-800">${formattedDate}</span>
|
||||
<span class="text-sm text-gray-500">(${issues.length}건)</span>
|
||||
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">업로드일</span>
|
||||
<span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">관리함 진입일</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content mt-4" id="content-${dateKey}">
|
||||
@@ -468,14 +475,14 @@
|
||||
|
||||
// 부적합명 추출 (첫 번째 줄)
|
||||
function getIssueTitle(issue) {
|
||||
const description = issue.final_description || issue.description || '';
|
||||
const description = issue.description || issue.final_description || '';
|
||||
const lines = description.split('\n');
|
||||
return lines[0] || '부적합명 없음';
|
||||
}
|
||||
|
||||
// 상세 내용 추출 (두 번째 줄부터)
|
||||
function getIssueDetail(issue) {
|
||||
const description = issue.final_description || issue.description || '';
|
||||
const description = issue.description || issue.final_description || '';
|
||||
const lines = description.split('\n');
|
||||
return lines.slice(1).join('\n') || '상세 내용 없음';
|
||||
}
|
||||
@@ -605,7 +612,7 @@
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="inline-flex items-center bg-gradient-to-r from-yellow-400 to-orange-400 text-white px-2 py-1 rounded-full text-xs font-medium shadow-sm">
|
||||
<i class="fas fa-tag mr-1"></i>
|
||||
${getCategoryText(issue.final_category || issue.category)}
|
||||
${getCategoryText(issue.category || issue.final_category)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -838,7 +845,7 @@
|
||||
if (selectedProject) {
|
||||
filterByProject();
|
||||
} else {
|
||||
loadDashboardData();
|
||||
initializeDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -812,7 +812,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 제목 -->
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3 cursor-pointer hover:text-blue-600 transition-colors" onclick="viewIssueDetail(${issue.id})">${issue.description}</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-3 cursor-pointer hover:text-blue-600 transition-colors" onclick="viewIssueDetail(${issue.id})">${issue.final_description || issue.description}</h3>
|
||||
|
||||
<!-- 상세 정보 그리드 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4 text-sm">
|
||||
@@ -822,7 +822,7 @@
|
||||
</div>
|
||||
<div class="flex items-center text-gray-600">
|
||||
<i class="fas fa-tag mr-2 text-green-500"></i>
|
||||
<span>${getCategoryText(issue.category)}</span>
|
||||
<span>${getCategoryText(issue.category || issue.final_category)}</span>
|
||||
</div>
|
||||
<div class="flex items-center text-gray-600">
|
||||
<i class="fas fa-camera mr-2 text-purple-500"></i>
|
||||
@@ -1075,10 +1075,10 @@
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium text-gray-900 mb-1">
|
||||
${issue.description}
|
||||
${issue.description || issue.final_description}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<span class="px-2 py-1 bg-gray-100 rounded">${getCategoryText(issue.category)}</span>
|
||||
<span class="px-2 py-1 bg-gray-100 rounded">${getCategoryText(issue.category || issue.final_category)}</span>
|
||||
<span>신고자: ${issue.reporter_name}</span>
|
||||
${issue.duplicate_count > 0 ? `<span class="text-orange-600">중복 ${issue.duplicate_count}건</span>` : ''}
|
||||
</div>
|
||||
@@ -1176,8 +1176,8 @@
|
||||
originalInfo.innerHTML = `
|
||||
<div class="space-y-2">
|
||||
<div><strong>프로젝트:</strong> ${project ? project.project_name : '미지정'}</div>
|
||||
<div><strong>카테고리:</strong> ${getCategoryText(issue.category)}</div>
|
||||
<div><strong>설명:</strong> ${issue.description}</div>
|
||||
<div><strong>카테고리:</strong> ${getCategoryText(issue.category || issue.final_category)}</div>
|
||||
<div><strong>설명:</strong> ${issue.description || issue.final_description}</div>
|
||||
<div><strong>등록자:</strong> ${issue.reporter?.username || '알 수 없음'}</div>
|
||||
<div><strong>등록일:</strong> ${new Date(issue.report_date).toLocaleDateString('ko-KR')}</div>
|
||||
</div>
|
||||
@@ -1196,12 +1196,13 @@
|
||||
reviewProjectSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// 현재 값들로 폼 초기화
|
||||
document.getElementById('reviewCategory').value = issue.category;
|
||||
// 기존 description을 title과 description으로 분리 (첫 번째 줄을 title로 사용)
|
||||
const lines = issue.description.split('\n');
|
||||
// 현재 값들로 폼 초기화 (최신 내용 우선 사용)
|
||||
document.getElementById('reviewCategory').value = issue.category || issue.final_category;
|
||||
// 최신 description을 title과 description으로 분리 (첫 번째 줄을 title로 사용)
|
||||
const currentDescription = issue.description || issue.final_description;
|
||||
const lines = currentDescription.split('\n');
|
||||
document.getElementById('reviewTitle').value = lines[0] || '';
|
||||
document.getElementById('reviewDescription').value = lines.slice(1).join('\n') || issue.description;
|
||||
document.getElementById('reviewDescription').value = lines.slice(1).join('\n') || currentDescription;
|
||||
|
||||
document.getElementById('reviewModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -693,14 +693,14 @@
|
||||
|
||||
// 부적합명 추출 (첫 번째 줄)
|
||||
function getIssueTitle(issue) {
|
||||
const description = issue.final_description || issue.description || '';
|
||||
const description = issue.description || issue.final_description || '';
|
||||
const lines = description.split('\n');
|
||||
return lines[0] || '부적합명 없음';
|
||||
}
|
||||
|
||||
// 상세 내용 추출 (두 번째 줄부터)
|
||||
function getIssueDetail(issue) {
|
||||
const description = issue.final_description || issue.description || '';
|
||||
const description = issue.description || issue.final_description || '';
|
||||
const lines = description.split('\n');
|
||||
return lines.slice(1).join('\n') || '상세 내용 없음';
|
||||
}
|
||||
@@ -833,7 +833,7 @@
|
||||
<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.final_category || issue.category)}
|
||||
${getCategoryText(issue.category || issue.final_category)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +142,24 @@
|
||||
<!-- 프로젝트 목록이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 비활성화된 프로젝트 섹션 -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 mt-6">
|
||||
<div class="flex justify-between items-center mb-4 cursor-pointer" onclick="toggleInactiveProjects()">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i id="inactiveToggleIcon" class="fas fa-chevron-down transition-transform duration-200"></i>
|
||||
<h2 class="text-lg font-semibold text-gray-600">비활성화된 프로젝트</h2>
|
||||
<span id="inactiveProjectCount" class="bg-gray-100 text-gray-600 px-2 py-1 rounded-full text-sm font-medium">0</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<i class="fas fa-info-circle mr-1"></i>클릭하여 펼치기/접기
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="inactiveProjectsList" class="space-y-3">
|
||||
<!-- 비활성화된 프로젝트 목록이 여기에 표시됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- API 스크립트 먼저 로드 (최강 캐시 무력화) -->
|
||||
@@ -170,40 +188,50 @@
|
||||
console.log('🚀 캐시 버스터:', cacheBuster);
|
||||
</script>
|
||||
|
||||
<!-- API 스크립트 동적 로딩 -->
|
||||
<!-- 메인 스크립트 -->
|
||||
<script>
|
||||
// 최강 캐시 무력화로 API 스크립트 로드
|
||||
const timestamp = new Date().getTime();
|
||||
const random1 = Math.random() * 1000000;
|
||||
const random2 = Math.floor(Math.random() * 1000000);
|
||||
const cacheBuster = `${timestamp}-${random1}-${random2}`;
|
||||
|
||||
// 기존 api.js 스크립트 제거
|
||||
const existingScripts = document.querySelectorAll('script[src*="api.js"]');
|
||||
existingScripts.forEach(script => script.remove());
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = `/static/js/api.js?v=${timestamp}&cb=${random1}&bust=${random2}&force=${Date.now()}`;
|
||||
script.onload = function() {
|
||||
console.log('✅ API 스크립트 로드 완료 (project-management.html)');
|
||||
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
|
||||
// API 로드 후 인증 확인 시작
|
||||
checkAdminAccess();
|
||||
};
|
||||
script.setAttribute('cache-control', 'no-cache, no-store, must-revalidate');
|
||||
script.setAttribute('pragma', 'no-cache');
|
||||
script.setAttribute('expires', '0');
|
||||
script.crossOrigin = 'anonymous';
|
||||
document.head.appendChild(script);
|
||||
|
||||
console.log('📱 프로젝트 관리 - 캐시 버스터:', cacheBuster);
|
||||
// 관리자 권한 확인 함수
|
||||
async function checkAdminAccess() {
|
||||
try {
|
||||
const currentUser = await AuthAPI.getCurrentUser();
|
||||
if (!currentUser || currentUser.role !== 'admin') {
|
||||
alert('관리자만 접근할 수 있습니다.');
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
}
|
||||
// 권한 확인 후 페이지 초기화
|
||||
initializeProjectManagement();
|
||||
} catch (error) {
|
||||
console.error('권한 확인 실패:', error);
|
||||
alert('로그인이 필요합니다.');
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 관리 페이지 초기화 함수
|
||||
async function initializeProjectManagement() {
|
||||
try {
|
||||
console.log('🚀 프로젝트 관리 페이지 초기화 시작');
|
||||
|
||||
// 헤더 애니메이션 시작
|
||||
animateHeaderAppearance();
|
||||
|
||||
// 프로젝트 목록 로드
|
||||
await loadProjects();
|
||||
|
||||
console.log('✅ 프로젝트 관리 페이지 초기화 완료');
|
||||
} catch (error) {
|
||||
console.error('❌ 프로젝트 관리 페이지 초기화 실패:', error);
|
||||
alert('페이지 로드 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
</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;
|
||||
|
||||
// 애니메이션 함수들
|
||||
@@ -309,7 +337,7 @@
|
||||
console.log('프로젝트 로드 시작 (API)');
|
||||
|
||||
try {
|
||||
// API에서 프로젝트 로드
|
||||
// API에서 모든 프로젝트 로드 (활성/비활성 모두)
|
||||
const apiProjects = await ProjectsAPI.getAll(false);
|
||||
|
||||
// API 데이터를 그대로 사용 (필드명 통일)
|
||||
@@ -368,11 +396,16 @@
|
||||
|
||||
// 프로젝트 목록 표시
|
||||
function displayProjectList() {
|
||||
const container = document.getElementById('projectsList');
|
||||
container.innerHTML = '';
|
||||
const activeContainer = document.getElementById('projectsList');
|
||||
const inactiveContainer = document.getElementById('inactiveProjectsList');
|
||||
const inactiveCount = document.getElementById('inactiveProjectCount');
|
||||
|
||||
activeContainer.innerHTML = '';
|
||||
inactiveContainer.innerHTML = '';
|
||||
|
||||
if (projects.length === 0) {
|
||||
container.innerHTML = '<p class="text-gray-500 text-center py-8">등록된 프로젝트가 없습니다.</p>';
|
||||
activeContainer.innerHTML = '<p class="text-gray-500 text-center py-8">등록된 프로젝트가 없습니다.</p>';
|
||||
inactiveCount.textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -383,15 +416,12 @@
|
||||
console.log('전체 프로젝트:', projects.length, '개');
|
||||
console.log('활성 프로젝트:', activeProjects.length, '개');
|
||||
console.log('비활성 프로젝트:', inactiveProjects.length, '개');
|
||||
console.log('비활성 프로젝트 목록:', inactiveProjects);
|
||||
|
||||
// 비활성 프로젝트 개수 업데이트
|
||||
inactiveCount.textContent = inactiveProjects.length;
|
||||
|
||||
// 활성 프로젝트 표시
|
||||
if (activeProjects.length > 0) {
|
||||
const activeHeader = document.createElement('div');
|
||||
activeHeader.className = 'mb-4';
|
||||
activeHeader.innerHTML = '<h3 class="text-md font-semibold text-green-700"><i class="fas fa-check-circle mr-2"></i>활성 프로젝트 (' + activeProjects.length + '개)</h3>';
|
||||
container.appendChild(activeHeader);
|
||||
|
||||
activeProjects.forEach(project => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border border-green-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-green-50';
|
||||
@@ -421,20 +451,17 @@
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
activeContainer.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
activeContainer.innerHTML = '<p class="text-gray-500 text-center py-8">활성 프로젝트가 없습니다.</p>';
|
||||
}
|
||||
|
||||
// 비활성 프로젝트 표시
|
||||
if (inactiveProjects.length > 0) {
|
||||
const inactiveHeader = document.createElement('div');
|
||||
inactiveHeader.className = 'mb-4 mt-6';
|
||||
inactiveHeader.innerHTML = '<h3 class="text-md font-semibold text-gray-500"><i class="fas fa-pause-circle mr-2"></i>비활성 프로젝트 (' + inactiveProjects.length + '개)</h3>';
|
||||
container.appendChild(inactiveHeader);
|
||||
|
||||
inactiveProjects.forEach(project => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-gray-50 opacity-75';
|
||||
div.className = 'border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow mb-3 bg-gray-50';
|
||||
div.innerHTML = `
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
@@ -458,8 +485,28 @@
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
inactiveContainer.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
inactiveContainer.innerHTML = '<p class="text-gray-500 text-center py-8">비활성화된 프로젝트가 없습니다.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// 비활성 프로젝트 섹션 토글
|
||||
function toggleInactiveProjects() {
|
||||
const inactiveList = document.getElementById('inactiveProjectsList');
|
||||
const toggleIcon = document.getElementById('inactiveToggleIcon');
|
||||
|
||||
if (inactiveList.style.display === 'none') {
|
||||
// 펼치기
|
||||
inactiveList.style.display = 'block';
|
||||
toggleIcon.classList.remove('fa-chevron-right');
|
||||
toggleIcon.classList.add('fa-chevron-down');
|
||||
} else {
|
||||
// 접기
|
||||
inactiveList.style.display = 'none';
|
||||
toggleIcon.classList.remove('fa-chevron-down');
|
||||
toggleIcon.classList.add('fa-chevron-right');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ function checkPageAccess(pageName) {
|
||||
// 프로젝트 API
|
||||
const ProjectsAPI = {
|
||||
getAll: (activeOnly = false) => {
|
||||
const params = activeOnly ? '?active_only=true' : '';
|
||||
const params = `?active_only=${activeOnly}`;
|
||||
return apiRequest(`/projects/${params}`);
|
||||
},
|
||||
|
||||
|
||||
@@ -293,11 +293,7 @@ class App {
|
||||
* 이벤트 리스너 등록
|
||||
*/
|
||||
registerEventListeners() {
|
||||
// 비밀번호 변경 폼
|
||||
document.getElementById('passwordChangeForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.handlePasswordChange();
|
||||
});
|
||||
// 비밀번호 변경은 CommonHeader에서 처리
|
||||
|
||||
// 모바일 반응형
|
||||
window.addEventListener('resize', () => {
|
||||
@@ -358,46 +354,7 @@ class App {
|
||||
document.getElementById('mobileOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 모달 표시
|
||||
*/
|
||||
showPasswordChangeModal() {
|
||||
document.getElementById('passwordModal').classList.remove('hidden');
|
||||
document.getElementById('passwordModal').classList.add('flex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 모달 숨기기
|
||||
*/
|
||||
hidePasswordChangeModal() {
|
||||
document.getElementById('passwordModal').classList.add('hidden');
|
||||
document.getElementById('passwordModal').classList.remove('flex');
|
||||
document.getElementById('passwordChangeForm').reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 처리
|
||||
*/
|
||||
async handlePasswordChange() {
|
||||
const currentPassword = document.getElementById('currentPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
this.showError('새 비밀번호가 일치하지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// API 호출 (구현 필요)
|
||||
// await AuthAPI.changePassword(currentPassword, newPassword);
|
||||
|
||||
this.showSuccess('비밀번호가 성공적으로 변경되었습니다.');
|
||||
this.hidePasswordChangeModal();
|
||||
} catch (error) {
|
||||
this.showError('비밀번호 변경에 실패했습니다.');
|
||||
}
|
||||
}
|
||||
// 비밀번호 변경 기능은 CommonHeader.js에서 처리됩니다.
|
||||
|
||||
/**
|
||||
* 로그아웃
|
||||
@@ -472,13 +429,7 @@ function toggleSidebar() {
|
||||
window.app.toggleSidebar();
|
||||
}
|
||||
|
||||
function showPasswordChangeModal() {
|
||||
window.app.showPasswordChangeModal();
|
||||
}
|
||||
|
||||
function hidePasswordChangeModal() {
|
||||
window.app.hidePasswordChangeModal();
|
||||
}
|
||||
// 비밀번호 변경 기능은 CommonHeader.showPasswordModal()을 사용합니다.
|
||||
|
||||
function logout() {
|
||||
window.app.logout();
|
||||
|
||||
@@ -479,8 +479,151 @@ class CommonHeader {
|
||||
* 비밀번호 변경 모달 표시
|
||||
*/
|
||||
static showPasswordModal() {
|
||||
// 비밀번호 변경 모달 구현 (기존 코드 재사용)
|
||||
alert('비밀번호 변경 기능은 관리자 페이지에서 이용해주세요.');
|
||||
// 기존 모달이 있으면 제거
|
||||
const existingModal = document.getElementById('passwordChangeModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// 비밀번호 변경 모달 생성
|
||||
const modalHTML = `
|
||||
<div id="passwordChangeModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]">
|
||||
<div class="bg-white rounded-xl p-6 w-96 max-w-md mx-4 shadow-2xl">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800">
|
||||
<i class="fas fa-key mr-2 text-blue-500"></i>비밀번호 변경
|
||||
</h3>
|
||||
<button onclick="CommonHeader.hidePasswordModal()" class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<i class="fas fa-times text-lg"></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="currentPasswordInput"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
required placeholder="현재 비밀번호를 입력하세요">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호</label>
|
||||
<input type="password" id="newPasswordInput"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
required minlength="6" placeholder="새 비밀번호 (최소 6자)">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">새 비밀번호 확인</label>
|
||||
<input type="password" id="confirmPasswordInput"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
required placeholder="새 비밀번호를 다시 입력하세요">
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button type="button" onclick="CommonHeader.hidePasswordModal()"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||||
취소
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="flex-1 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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
|
||||
// 폼 제출 이벤트 리스너 추가
|
||||
document.getElementById('passwordChangeForm').addEventListener('submit', CommonHeader.handlePasswordChange);
|
||||
|
||||
// ESC 키로 모달 닫기
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
CommonHeader.hidePasswordModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 모달 숨기기
|
||||
*/
|
||||
static hidePasswordModal() {
|
||||
const modal = document.getElementById('passwordChangeModal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 변경 처리
|
||||
*/
|
||||
static async handlePasswordChange(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const currentPassword = document.getElementById('currentPasswordInput').value;
|
||||
const newPassword = document.getElementById('newPasswordInput').value;
|
||||
const confirmPassword = document.getElementById('confirmPasswordInput').value;
|
||||
|
||||
// 새 비밀번호 확인
|
||||
if (newPassword !== confirmPassword) {
|
||||
CommonHeader.showToast('새 비밀번호가 일치하지 않습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
CommonHeader.showToast('새 비밀번호는 최소 6자 이상이어야 합니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// AuthAPI가 있는지 확인
|
||||
if (typeof AuthAPI === 'undefined') {
|
||||
throw new Error('AuthAPI가 로드되지 않았습니다.');
|
||||
}
|
||||
|
||||
// API를 통한 비밀번호 변경
|
||||
await AuthAPI.changePassword(currentPassword, newPassword);
|
||||
|
||||
CommonHeader.showToast('비밀번호가 성공적으로 변경되었습니다.', 'success');
|
||||
CommonHeader.hidePasswordModal();
|
||||
|
||||
} catch (error) {
|
||||
console.error('비밀번호 변경 실패:', error);
|
||||
CommonHeader.showToast('현재 비밀번호가 올바르지 않거나 변경에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토스트 메시지 표시
|
||||
*/
|
||||
static showToast(message, type = 'success') {
|
||||
// 기존 토스트 제거
|
||||
const existingToast = document.querySelector('.toast-message');
|
||||
if (existingToast) {
|
||||
existingToast.remove();
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast-message fixed bottom-4 right-4 px-4 py-3 rounded-lg text-white z-[10000] shadow-lg transform transition-all duration-300 ${
|
||||
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
||||
}`;
|
||||
|
||||
const icon = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
|
||||
toast.innerHTML = `<i class="fas ${icon} mr-2"></i>${message}`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 애니메이션 효과
|
||||
setTimeout(() => toast.classList.add('translate-x-0'), 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-x-full');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user