feat: 품목정보 상세 패널 추가 및 프로젝트/근태/권한 기능 확장
- Items: 우측에 이미지, 담당자, 입고/발주내역 패널 추가 (fItems 윈폼 동일) - Project: 목록 및 상세 다이얼로그 구현 - Kuntae: 오류검사/수정 기능 추가 - UserAuth: 사용자 권한 관리 페이지 추가 - UserGroup: 그룹정보 다이얼로그로 전환 - Header: 사용자 메뉴 서브메뉴 방향 수정, 즐겨찾기 기능 - Backend API: Items 상세/담당자/구매내역, 근태 오류검사, 프로젝트 목록 등 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Save, Trash2 } from 'lucide-react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { X, Save, Trash2, Upload, Clipboard, ImageIcon } from 'lucide-react';
|
||||
import { ItemInfo } from '@/types';
|
||||
import { comms } from '@/communication';
|
||||
|
||||
interface ItemEditDialogProps {
|
||||
item: ItemInfo | null;
|
||||
@@ -13,13 +14,169 @@ interface ItemEditDialogProps {
|
||||
export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: ItemEditDialogProps) {
|
||||
const [editData, setEditData] = useState<ItemInfo | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [imageData, setImageData] = useState<string | null>(null);
|
||||
const [imageLoading, setImageLoading] = useState(false);
|
||||
const [imageSaving, setImageSaving] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const dropZoneRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
setEditData({ ...item });
|
||||
setImageData(null);
|
||||
// 기존 품목인 경우 이미지 로드
|
||||
if (item.idx > 0) {
|
||||
loadImage(item.idx);
|
||||
}
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
// 이미지 로드
|
||||
const loadImage = async (idx: number) => {
|
||||
setImageLoading(true);
|
||||
try {
|
||||
const result = await comms.getItemImage(idx);
|
||||
if (result.Success && result.Data) {
|
||||
setImageData(`data:image/jpeg;base64,${result.Data}`);
|
||||
} else {
|
||||
setImageData(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('이미지 로드 실패:', error);
|
||||
setImageData(null);
|
||||
} finally {
|
||||
setImageLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ESC 키로 닫기
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// 이미지를 Base64로 변환
|
||||
const convertToBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
// 이미지 파일 처리
|
||||
const handleImageFile = async (file: File) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('이미지 파일만 업로드 가능합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = await convertToBase64(file);
|
||||
setImageData(base64);
|
||||
|
||||
// 기존 품목인 경우 바로 저장
|
||||
if (editData && editData.idx > 0) {
|
||||
setImageSaving(true);
|
||||
const result = await comms.saveItemImage(editData.idx, base64);
|
||||
if (!result.Success) {
|
||||
alert(result.Message || '이미지 저장 실패');
|
||||
}
|
||||
setImageSaving(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('이미지 처리 실패:', error);
|
||||
alert('이미지 처리에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 파일 선택
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
handleImageFile(file);
|
||||
}
|
||||
// 같은 파일 다시 선택 가능하도록
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
// 클립보드에서 붙여넣기
|
||||
const handlePaste = useCallback(async () => {
|
||||
try {
|
||||
const items = await navigator.clipboard.read();
|
||||
for (const item of items) {
|
||||
const imageType = item.types.find(type => type.startsWith('image/'));
|
||||
if (imageType) {
|
||||
const blob = await item.getType(imageType);
|
||||
const file = new File([blob], 'clipboard-image.png', { type: imageType });
|
||||
await handleImageFile(file);
|
||||
return;
|
||||
}
|
||||
}
|
||||
alert('클립보드에 이미지가 없습니다.');
|
||||
} catch (error) {
|
||||
console.error('클립보드 읽기 실패:', error);
|
||||
alert('클립보드에서 이미지를 가져올 수 없습니다.');
|
||||
}
|
||||
}, [editData]);
|
||||
|
||||
// 드래그 앤 드롭
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDrop = async (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
await handleImageFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
// 이미지 삭제
|
||||
const handleDeleteImage = async () => {
|
||||
if (!editData) return;
|
||||
|
||||
if (!confirm('이미지를 삭제하시겠습니까?')) return;
|
||||
|
||||
if (editData.idx > 0) {
|
||||
setImageSaving(true);
|
||||
const result = await comms.deleteItemImage(editData.idx);
|
||||
if (result.Success) {
|
||||
setImageData(null);
|
||||
} else {
|
||||
alert(result.Message || '이미지 삭제 실패');
|
||||
}
|
||||
setImageSaving(false);
|
||||
} else {
|
||||
setImageData(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen || !editData) return null;
|
||||
|
||||
const isNew = editData.idx === 0;
|
||||
@@ -48,10 +205,13 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* 배경 오버레이 */}
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onMouseDown={onClose} />
|
||||
|
||||
{/* 다이얼로그 */}
|
||||
<div className="relative bg-slate-800 rounded-xl shadow-2xl w-full max-w-lg mx-4 border border-white/10">
|
||||
{/* 다이얼로그 - 이미지 영역 포함해서 더 넓게 */}
|
||||
<div
|
||||
className="relative bg-slate-800 rounded-xl shadow-2xl w-full max-w-4xl mx-4 border border-white/10"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<h2 className="text-lg font-semibold text-white">
|
||||
@@ -65,145 +225,245 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="p-4 space-y-4 max-h-[60vh] overflow-auto">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* SID */}
|
||||
{/* 내용 - 좌우 레이아웃 */}
|
||||
<div className="flex max-h-[70vh]">
|
||||
{/* 왼쪽: 폼 필드 */}
|
||||
<div className="flex-1 p-4 space-y-4 overflow-auto">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* SID */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">SID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.sid}
|
||||
onChange={(e) => setEditData({ ...editData, sid: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 분류 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">분류</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.cate}
|
||||
onChange={(e) => setEditData({ ...editData, cate: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 품명 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">SID</label>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">품명</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.sid}
|
||||
onChange={(e) => setEditData({ ...editData, sid: e.target.value })}
|
||||
value={editData.name}
|
||||
onChange={(e) => setEditData({ ...editData, name: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 분류 */}
|
||||
{/* 모델 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">분류</label>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">모델</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.cate}
|
||||
onChange={(e) => setEditData({ ...editData, cate: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 품명 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">품명</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.name}
|
||||
onChange={(e) => setEditData({ ...editData, name: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 모델 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">모델</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.model}
|
||||
onChange={(e) => setEditData({ ...editData, model: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* 규격 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">규격</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.scale}
|
||||
onChange={(e) => setEditData({ ...editData, scale: e.target.value })}
|
||||
value={editData.model}
|
||||
onChange={(e) => setEditData({ ...editData, model: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 단위 */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{/* 규격 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">규격</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.scale}
|
||||
onChange={(e) => setEditData({ ...editData, scale: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 단위 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">단위</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.unit}
|
||||
onChange={(e) => setEditData({ ...editData, unit: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 단가 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">단가</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editData.price}
|
||||
onChange={(e) => setEditData({ ...editData, price: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 공급처 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">공급처</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.supply}
|
||||
onChange={(e) => setEditData({ ...editData, supply: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 제조사 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">제조사</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.manu}
|
||||
onChange={(e) => setEditData({ ...editData, manu: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 보관장소 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">단위</label>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">보관장소</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.unit}
|
||||
onChange={(e) => setEditData({ ...editData, unit: e.target.value })}
|
||||
value={editData.storage}
|
||||
onChange={(e) => setEditData({ ...editData, storage: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 단가 */}
|
||||
{/* 메모 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">단가</label>
|
||||
<input
|
||||
type="number"
|
||||
value={editData.price}
|
||||
onChange={(e) => setEditData({ ...editData, price: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white text-right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* 공급처 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">공급처</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.supply}
|
||||
onChange={(e) => setEditData({ ...editData, supply: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">메모</label>
|
||||
<textarea
|
||||
value={editData.memo}
|
||||
onChange={(e) => setEditData({ ...editData, memo: e.target.value })}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 제조사 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">제조사</label>
|
||||
{/* 비활성화 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editData.manu}
|
||||
onChange={(e) => setEditData({ ...editData, manu: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
type="checkbox"
|
||||
id="disable"
|
||||
checked={editData.disable}
|
||||
onChange={(e) => setEditData({ ...editData, disable: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-white/20 bg-white/10"
|
||||
/>
|
||||
<label htmlFor="disable" className="text-sm text-white/70">비활성화</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 보관장소 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">보관장소</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editData.storage}
|
||||
onChange={(e) => setEditData({ ...editData, storage: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
{/* 오른쪽: 이미지 영역 */}
|
||||
<div className="w-72 p-4 border-l border-white/10 flex flex-col">
|
||||
<label className="block text-sm font-medium text-white/70 mb-2">이미지</label>
|
||||
|
||||
{/* 메모 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white/70 mb-1">메모</label>
|
||||
<textarea
|
||||
value={editData.memo}
|
||||
onChange={(e) => setEditData({ ...editData, memo: e.target.value })}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white resize-none"
|
||||
/>
|
||||
</div>
|
||||
{/* 이미지 드롭존 */}
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
className={`flex-1 min-h-[200px] rounded-lg border-2 border-dashed transition-colors flex items-center justify-center overflow-hidden ${
|
||||
isDragging
|
||||
? 'border-blue-400 bg-blue-500/20'
|
||||
: 'border-white/20 bg-white/5 hover:border-white/40'
|
||||
}`}
|
||||
>
|
||||
{imageLoading ? (
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white/50 mx-auto mb-2"></div>
|
||||
<p className="text-white/50 text-sm">로딩 중...</p>
|
||||
</div>
|
||||
) : imageSaving ? (
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400 mx-auto mb-2"></div>
|
||||
<p className="text-white/50 text-sm">저장 중...</p>
|
||||
</div>
|
||||
) : imageData ? (
|
||||
<img
|
||||
src={imageData}
|
||||
alt="품목 이미지"
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center p-4">
|
||||
<ImageIcon className="w-12 h-12 text-white/30 mx-auto mb-2" />
|
||||
<p className="text-white/50 text-sm">
|
||||
이미지를 드래그하거나<br />
|
||||
아래 버튼을 사용하세요
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 비활성화 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disable"
|
||||
checked={editData.disable}
|
||||
onChange={(e) => setEditData({ ...editData, disable: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-white/20 bg-white/10"
|
||||
/>
|
||||
<label htmlFor="disable" className="text-sm text-white/70">비활성화</label>
|
||||
{/* 이미지 버튼들 */}
|
||||
<div className="mt-3 space-y-2">
|
||||
{/* 파일 선택 */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={imageSaving}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white/80 hover:text-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
파일 선택
|
||||
</button>
|
||||
|
||||
{/* 붙여넣기 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePaste}
|
||||
disabled={imageSaving}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white/80 hover:text-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Clipboard className="w-4 h-4" />
|
||||
붙여넣기
|
||||
</button>
|
||||
|
||||
{/* 이미지 삭제 */}
|
||||
{imageData && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDeleteImage}
|
||||
disabled={imageSaving}
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-red-600/20 hover:bg-red-600/40 rounded-lg text-red-400 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
이미지 삭제
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 신규 품목 안내 */}
|
||||
{isNew && imageData && (
|
||||
<p className="text-xs text-yellow-400/80 text-center">
|
||||
* 품목 저장 후 이미지가 적용됩니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user