Fix: 페이지 간 이동 시 로그아웃 문제 해결 및 기능 개선
- 토큰 저장 키 통일 (access_token으로 일관성 확보) - 일일공수 페이지 API 스크립트 로딩 순서 수정 - 프로젝트 관리 페이지 비활성 프로젝트 표시 문제 해결 - 업로드 카테고리에 '기타' 항목 추가 (백엔드 schemas.py 포함) - 비밀번호 변경 기능 API 연동으로 수정 - 프로젝트 드롭다운 z-index 문제 해결 - CORS 설정 및 Nginx 구성 개선 - 비밀번호 해싱 방식 pbkdf2_sha256으로 변경 (bcrypt 72바이트 제한 해결)
This commit is contained in:
@@ -92,6 +92,40 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 모바일에서 프로젝트 드롭다운 강제 표시 */
|
||||
@media (max-width: 768px) {
|
||||
#projectSelect,
|
||||
select[id="projectSelect"] {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
width: 100% !important;
|
||||
min-height: 44px !important;
|
||||
height: auto !important;
|
||||
padding: 12px !important;
|
||||
font-size: 16px !important;
|
||||
border: 2px solid #3b82f6 !important;
|
||||
border-radius: 8px !important;
|
||||
background-color: white !important;
|
||||
color: black !important;
|
||||
-webkit-appearance: menulist !important;
|
||||
-moz-appearance: menulist !important;
|
||||
appearance: menulist !important;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
|
||||
position: relative !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
#projectSelect option,
|
||||
select[id="projectSelect"] option {
|
||||
display: block !important;
|
||||
padding: 8px !important;
|
||||
font-size: 16px !important;
|
||||
color: black !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -293,7 +327,7 @@
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<i class="fas fa-folder-open mr-1"></i>프로젝트
|
||||
</label>
|
||||
<select id="projectSelect" class="input-field w-full px-4 py-2 rounded-lg" required>
|
||||
<select id="projectSelect" class="input-field w-full px-4 py-2 rounded-lg relative z-10" required>
|
||||
<option value="">프로젝트를 선택하세요</option>
|
||||
<!-- 활성 프로젝트들이 여기에 로드됩니다 -->
|
||||
</select>
|
||||
@@ -308,6 +342,7 @@
|
||||
<option value="design_error">설계미스</option>
|
||||
<option value="incoming_defect">입고자재 불량</option>
|
||||
<option value="inspection_miss">검사미스</option>
|
||||
<option value="etc">기타</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -415,7 +450,38 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/api.js?v=20251024m"></script>
|
||||
<script>
|
||||
// 최강 캐시 무력화 (API URL 수정 반영)
|
||||
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 스크립트 로드 완료');
|
||||
console.log('🔍 API_BASE_URL:', typeof API_BASE_URL !== 'undefined' ? API_BASE_URL : 'undefined');
|
||||
console.log('🌐 현재 hostname:', window.location.hostname);
|
||||
console.log('🔗 현재 protocol:', window.location.protocol);
|
||||
// API 로드 후 초기화 시작
|
||||
initializeApp();
|
||||
};
|
||||
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);
|
||||
console.log('📱 화면 크기:', window.innerWidth, 'x', window.innerHeight);
|
||||
console.log('📱 User Agent:', navigator.userAgent);
|
||||
</script>
|
||||
<script src="/static/js/image-utils.js?v=20250917"></script>
|
||||
<script src="/static/js/date-utils.js?v=20250917"></script>
|
||||
<script>
|
||||
@@ -423,26 +489,48 @@
|
||||
let currentPhotos = [];
|
||||
let issues = [];
|
||||
|
||||
// 페이지 로드 시 인증 체크
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const user = TokenManager.getUser();
|
||||
if (user) {
|
||||
currentUser = user;
|
||||
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
document.getElementById('mainScreen').classList.remove('hidden');
|
||||
|
||||
// 권한에 따른 메뉴 표시/숨김
|
||||
updateNavigation();
|
||||
|
||||
// 프로젝트 로드
|
||||
loadProjects();
|
||||
|
||||
loadIssues();
|
||||
|
||||
// URL 해시 처리
|
||||
handleUrlHash();
|
||||
// API 로드 후 앱 초기화
|
||||
async function initializeApp() {
|
||||
console.log('🚀 앱 초기화 시작');
|
||||
|
||||
// 토큰이 있으면 사용자 정보 가져오기
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
try {
|
||||
// 토큰으로 사용자 정보 가져오기 (API 호출)
|
||||
const user = await AuthAPI.getCurrentUser();
|
||||
currentUser = user;
|
||||
|
||||
// localStorage에도 백업 저장
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
|
||||
document.getElementById('userDisplay').textContent = user.full_name || user.username;
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
document.getElementById('mainScreen').classList.remove('hidden');
|
||||
|
||||
// 권한에 따른 메뉴 표시/숨김
|
||||
updateNavigation();
|
||||
|
||||
// 프로젝트 로드
|
||||
await loadProjects();
|
||||
|
||||
loadIssues();
|
||||
|
||||
// URL 해시 처리
|
||||
handleUrlHash();
|
||||
|
||||
} catch (error) {
|
||||
console.error('토큰 검증 실패:', error);
|
||||
// 토큰이 유효하지 않으면 로그아웃
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('currentUser');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DOM 로드 완료 시 대기 (API 스크립트가 로드되면 initializeApp 호출됨)
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('📄 DOM 로드 완료 - API 스크립트 로딩 대기 중...');
|
||||
});
|
||||
|
||||
// 로그인
|
||||
@@ -454,6 +542,11 @@
|
||||
try {
|
||||
const data = await AuthAPI.login(userId, password);
|
||||
currentUser = data.user;
|
||||
|
||||
// 토큰과 사용자 정보 저장
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
||||
|
||||
document.getElementById('userDisplay').textContent = currentUser.full_name || currentUser.username;
|
||||
document.getElementById('loginScreen').classList.add('hidden');
|
||||
document.getElementById('mainScreen').classList.remove('hidden');
|
||||
@@ -461,6 +554,9 @@
|
||||
// 권한에 따른 메뉴 표시/숨김
|
||||
updateNavigation();
|
||||
|
||||
// 프로젝트 로드
|
||||
await loadProjects();
|
||||
|
||||
loadIssues();
|
||||
|
||||
// URL 해시 처리
|
||||
@@ -484,7 +580,7 @@
|
||||
const dailyWorkBtn = document.getElementById('dailyWorkBtn');
|
||||
|
||||
if (currentUser.role === 'admin') {
|
||||
// 관리자는 모든 메뉴 표시
|
||||
// 관리자는 모든 메뉴 표시 (비밀번호 변경은 사용자 관리 페이지에서)
|
||||
listBtn.style.display = '';
|
||||
summaryBtn.style.display = '';
|
||||
projectBtn.style.display = '';
|
||||
@@ -492,7 +588,7 @@
|
||||
adminBtn.style.display = '';
|
||||
adminBtn.innerHTML = '<i class="fas fa-users-cog mr-2"></i>사용자 관리';
|
||||
} else {
|
||||
// 일반 사용자는 제한된 메뉴만 표시
|
||||
// 일반 사용자는 제한된 메뉴만 표시 (비밀번호 변경 버튼 표시)
|
||||
listBtn.style.display = 'none';
|
||||
summaryBtn.style.display = 'none';
|
||||
projectBtn.style.display = 'none';
|
||||
@@ -545,6 +641,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 부적합 등록 섹션으로 전환 시 프로젝트 다시 로드 (모바일 대응)
|
||||
if (section === 'report') {
|
||||
setTimeout(async () => {
|
||||
const projectSelect = document.getElementById('projectSelect');
|
||||
if (!projectSelect || projectSelect.options.length <= 1) {
|
||||
console.log('부적합 등록 섹션 - 프로젝트 다시 로드');
|
||||
await loadProjects();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 섹션별 초기화
|
||||
if (section === 'list') {
|
||||
// 데이터가 없으면 먼저 로드
|
||||
@@ -751,7 +858,14 @@
|
||||
const submitBtn = e.target.querySelector('button[type="submit"]');
|
||||
const originalBtnContent = submitBtn.innerHTML;
|
||||
const description = document.getElementById('description').value.trim();
|
||||
const projectId = document.getElementById('projectSelect').value;
|
||||
const projectSelect = document.getElementById('projectSelect');
|
||||
const projectId = projectSelect.value;
|
||||
|
||||
// 프로젝트 드롭다운이 비어있으면 다시 로드
|
||||
if (projectSelect.options.length <= 1) {
|
||||
console.log('프로젝트 드롭다운이 비어있음, 다시 로드 중...');
|
||||
await loadProjects();
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
alert('프로젝트를 선택해주세요.');
|
||||
@@ -822,7 +936,10 @@
|
||||
console.log('DEBUG: parseInt(projectId):', parseInt(projectId));
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// 부적합 사항은 API로 직접 업로드 (정상 작동 중)
|
||||
await IssuesAPI.create(issueData);
|
||||
|
||||
const uploadTime = Date.now() - startTime;
|
||||
|
||||
updateProgress(90);
|
||||
@@ -881,23 +998,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 로드
|
||||
// 프로젝트 로드 (API 사용)
|
||||
async function loadProjects() {
|
||||
// 1. 즉시 localStorage에서 로드 (빠른 응답)
|
||||
const saved = localStorage.getItem('work-report-projects');
|
||||
let projects = [];
|
||||
console.log('=== 프로젝트 로드 시작 (API) ===');
|
||||
|
||||
if (saved) {
|
||||
projects = JSON.parse(saved);
|
||||
displayProjectsInUI(projects);
|
||||
}
|
||||
|
||||
// 2. 백그라운드에서 API 동기화
|
||||
try {
|
||||
// API에서 프로젝트 로드 (인증 없이)
|
||||
const apiProjects = await ProjectsAPI.getAll(false);
|
||||
|
||||
// API 데이터를 localStorage 형식으로 변환
|
||||
const syncedProjects = apiProjects.map(p => ({
|
||||
// API 데이터를 UI 형식으로 변환
|
||||
const projects = apiProjects.map(p => ({
|
||||
id: p.id,
|
||||
jobNo: p.job_no,
|
||||
projectName: p.project_name,
|
||||
@@ -906,23 +1016,26 @@
|
||||
createdByName: '관리자'
|
||||
}));
|
||||
|
||||
// localStorage 업데이트
|
||||
localStorage.setItem('work-report-projects', JSON.stringify(syncedProjects));
|
||||
|
||||
// UI 다시 업데이트 (동기화된 데이터로)
|
||||
displayProjectsInUI(syncedProjects);
|
||||
console.log('✅ API에서 프로젝트 로드:', projects.length, '개');
|
||||
displayProjectsInUI(projects);
|
||||
|
||||
} catch (error) {
|
||||
// API 실패해도 localStorage 데이터로 계속 동작
|
||||
console.log('API 동기화 실패, localStorage 데이터 사용:', error.message);
|
||||
console.error('❌ API 로드 실패:', error);
|
||||
|
||||
// API 실패 시 빈 배열 표시
|
||||
console.log('❌ API 로드 실패 - 프로젝트 없음');
|
||||
displayProjectsInUI([]);
|
||||
}
|
||||
}
|
||||
|
||||
function displayProjectsInUI(projects) {
|
||||
const activeProjects = projects.filter(p => p.isActive);
|
||||
console.log('displayProjectsInUI - 활성 프로젝트:', activeProjects);
|
||||
|
||||
// 부적합 등록 폼의 프로젝트 선택 (활성 프로젝트만)
|
||||
const projectSelect = document.getElementById('projectSelect');
|
||||
console.log('projectSelect 요소:', projectSelect);
|
||||
|
||||
if (projectSelect) {
|
||||
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
||||
|
||||
@@ -931,7 +1044,56 @@
|
||||
option.value = project.id;
|
||||
option.textContent = `${project.jobNo} - ${project.projectName}`;
|
||||
projectSelect.appendChild(option);
|
||||
console.log('프로젝트 옵션 추가:', project.jobNo, project.projectName);
|
||||
});
|
||||
|
||||
// 디버깅: 프로젝트 드롭다운 상태 확인
|
||||
console.log('프로젝트 드롭다운 상태:');
|
||||
console.log('- 현재 사용자:', window.currentUser);
|
||||
console.log('- 드롭다운 표시 여부:', projectSelect.style.display);
|
||||
console.log('- 드롭다운 옵션 수:', projectSelect.options.length);
|
||||
console.log('- 부모 요소 표시 여부:', projectSelect.parentElement.style.display);
|
||||
|
||||
// 모바일에서 select 요소 강제 표시
|
||||
if (window.innerWidth <= 768) {
|
||||
// 모바일에서 강제 스타일 적용
|
||||
projectSelect.style.display = 'block';
|
||||
projectSelect.style.visibility = 'visible';
|
||||
projectSelect.style.opacity = '1';
|
||||
projectSelect.style.width = '100%';
|
||||
projectSelect.style.height = 'auto';
|
||||
projectSelect.style.minHeight = '40px';
|
||||
|
||||
// 부모 요소도 강제 표시
|
||||
if (projectSelect.parentElement) {
|
||||
projectSelect.parentElement.style.display = 'block';
|
||||
projectSelect.parentElement.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
console.log('모바일 강제 스타일 적용 완료');
|
||||
|
||||
// 모바일에서 드롭다운 클릭 이벤트 강제 바인딩
|
||||
projectSelect.addEventListener('click', async function() {
|
||||
console.log('모바일 드롭다운 클릭됨, 옵션 수:', this.options.length);
|
||||
// 옵션이 없으면 다시 로드
|
||||
if (this.options.length <= 1) {
|
||||
console.log('옵션이 없어서 프로젝트 다시 로드');
|
||||
await loadProjects();
|
||||
}
|
||||
});
|
||||
|
||||
// 모바일에서 드롭다운 포커스 이벤트
|
||||
projectSelect.addEventListener('focus', function() {
|
||||
console.log('모바일 드롭다운 포커스, 옵션 수:', this.options.length);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error('projectSelect 요소를 찾을 수 없습니다!');
|
||||
// 모바일에서 DOM이 준비되지 않았을 수 있으므로 재시도
|
||||
setTimeout(() => {
|
||||
console.log('projectSelect 재시도...');
|
||||
displayProjectsInUI(projects);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 목록 관리의 프로젝트 필터 (모든 프로젝트)
|
||||
@@ -1189,6 +1351,7 @@
|
||||
<option value="design_error" ${issue.category === 'design_error' ? 'selected' : ''}>설계미스</option>
|
||||
<option value="incoming_defect" ${issue.category === 'incoming_defect' ? 'selected' : ''}>입고자재 불량</option>
|
||||
<option value="inspection_miss" ${issue.category === 'inspection_miss' ? 'selected' : ''}>검사미스</option>
|
||||
<option value="etc" ${issue.category === 'etc' ? 'selected' : ''}>기타</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -1459,7 +1622,7 @@
|
||||
// 비밀번호 변경 모달 표시
|
||||
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.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]';
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
};
|
||||
@@ -1522,56 +1685,16 @@
|
||||
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));
|
||||
// API를 통한 비밀번호 변경
|
||||
await AuthAPI.changePassword(currentPassword, newPassword);
|
||||
|
||||
showToastMessage('비밀번호가 성공적으로 변경되었습니다.');
|
||||
document.querySelector('.fixed').remove(); // 모달 닫기
|
||||
|
||||
} catch (error) {
|
||||
alert('비밀번호 변경에 실패했습니다: ' + error.message);
|
||||
console.error('비밀번호 변경 실패:', error);
|
||||
showToastMessage('현재 비밀번호가 올바르지 않거나 변경에 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1637,34 +1760,22 @@
|
||||
// 프로젝트별 일일 공수 데이터 계산
|
||||
let dailyWorkTotal = 0;
|
||||
|
||||
// localStorage의 프로젝트별 데이터 우선 사용 (프로젝트별 분리 지원)
|
||||
const dailyWorkData = JSON.parse(localStorage.getItem('daily-work-data') || '[]');
|
||||
if (selectedProjectId) {
|
||||
// 선택된 프로젝트의 일일 공수만 합계
|
||||
dailyWorkData.forEach(dayWork => {
|
||||
if (dayWork.projects) {
|
||||
dayWork.projects.forEach(project => {
|
||||
if (project.projectId == selectedProjectId || project.projectId.toString() === selectedProjectId.toString()) {
|
||||
dailyWorkTotal += project.hours || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log(`프로젝트 ID ${selectedProjectId}의 총 일일공수:`, dailyWorkTotal);
|
||||
} else {
|
||||
// 전체 프로젝트의 일일 공수 합계
|
||||
try {
|
||||
// 백엔드 API에서 전체 일일공수 데이터 가져오기
|
||||
// API에서 일일 공수 데이터 가져오기
|
||||
try {
|
||||
if (selectedProjectId) {
|
||||
// 선택된 프로젝트의 일일 공수만 가져오기
|
||||
const projectDailyWork = await DailyWorkAPI.getAll({ project_id: selectedProjectId });
|
||||
dailyWorkTotal = projectDailyWork.reduce((sum, work) => sum + (work.total_hours || 0), 0);
|
||||
console.log(`프로젝트 ID ${selectedProjectId}의 총 일일공수:`, dailyWorkTotal);
|
||||
} else {
|
||||
// 전체 프로젝트의 일일 공수 합계
|
||||
const apiDailyWork = await DailyWorkAPI.getAll();
|
||||
dailyWorkTotal = apiDailyWork.reduce((sum, work) => sum + (work.total_hours || 0), 0);
|
||||
console.log('API에서 가져온 전체 총 일일공수:', dailyWorkTotal);
|
||||
} catch (error) {
|
||||
// API 실패 시 localStorage 사용
|
||||
dailyWorkData.forEach(dayWork => {
|
||||
dailyWorkTotal += dayWork.totalHours || 0;
|
||||
});
|
||||
console.log('localStorage에서 가져온 전체 총 일일공수:', dailyWorkTotal);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('일일 공수 API 호출 실패:', error);
|
||||
dailyWorkTotal = 0;
|
||||
}
|
||||
|
||||
// 부적합 사항 해결 시간 계산 (필터링된 이슈만)
|
||||
@@ -1681,10 +1792,12 @@
|
||||
// 선택된 프로젝트 정보
|
||||
let projectInfo = '전체 프로젝트';
|
||||
if (selectedProjectId) {
|
||||
const projects = JSON.parse(localStorage.getItem('work-report-projects') || '[]');
|
||||
const selectedProject = projects.find(p => p.id == selectedProjectId);
|
||||
if (selectedProject) {
|
||||
projectInfo = `${selectedProject.jobNo} - ${selectedProject.projectName}`;
|
||||
try {
|
||||
const selectedProject = await ProjectsAPI.get(selectedProjectId);
|
||||
projectInfo = `${selectedProject.job_no} - ${selectedProject.project_name}`;
|
||||
} catch (error) {
|
||||
console.error('프로젝트 정보 로드 실패:', error);
|
||||
projectInfo = `프로젝트 ID: ${selectedProjectId}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user