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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -76,6 +76,18 @@ async def update_issue(
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
elif field == 'final_description' and value:
|
||||
# final_description 업데이트 시 description도 함께 업데이트
|
||||
issue.final_description = value
|
||||
issue.description = value
|
||||
print(f"✅ final_description 및 description 업데이트: {value[:50]}...")
|
||||
continue
|
||||
elif field == 'final_category' and value:
|
||||
# final_category 업데이트 시 category도 함께 업데이트
|
||||
issue.final_category = value
|
||||
issue.category = value
|
||||
print(f"✅ final_category 및 category 업데이트: {value}")
|
||||
continue
|
||||
elif field == 'expected_completion_date' and value:
|
||||
# 날짜 필드 처리
|
||||
if not value.endswith('T00:00:00'):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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