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:
hyungi
2025-10-26 15:28:23 +09:00
parent fd0579805c
commit c16fc53f3b
25 changed files with 295 additions and 169 deletions

View File

@@ -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'):

View File

@@ -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>

View File

@@ -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 => ({

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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');
}

View File

@@ -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>

View File

@@ -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');
}
}

View File

@@ -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}`);
},

View File

@@ -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();

View File

@@ -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);
}
/**