wing-ops/frontend/src/tabs/admin/components/AssetUploadPanel.tsx

258 lines
12 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import { fetchUploadLogs } from '@tabs/assets/services/assetsApi';
import type { UploadLogItem } from '@tabs/assets/services/assetsApi';
const ASSET_CATEGORIES = ['전체', '방제선', '유회수기', '이송펌프', '방제차량', '살포장치', '오일붐', '흡착재', '기타'];
const JURISDICTIONS = ['전체', '남해청', '서해청', '중부청', '동해청', '제주청'];
const PERM_ITEMS = [
{ icon: '👑', role: '시스템관리자', desc: '전체 자산 업로드/삭제 가능', bg: 'rgba(245,158,11,0.15)', color: 'text-yellow-400' },
{ icon: '🔧', role: '운영관리자', desc: '관할청 내 자산 업로드 가능', bg: 'rgba(6,182,212,0.15)', color: 'text-primary-cyan' },
{ icon: '👁', role: '조회자', desc: '현황 조회만 가능', bg: 'rgba(148,163,184,0.15)', color: 'text-text-2' },
{ icon: '🚫', role: '게스트', desc: '접근 불가', bg: 'rgba(239,68,68,0.1)', color: 'text-red-400' },
];
function formatDate(dtm: string) {
const d = new Date(dtm);
if (isNaN(d.getTime())) return dtm;
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
function AssetUploadPanel() {
const [assetCategory, setAssetCategory] = useState('전체');
const [jurisdiction, setJurisdiction] = useState('전체');
const [uploadMode, setUploadMode] = useState<'add' | 'replace'>('add');
const [uploaded, setUploaded] = useState(false);
const [uploadHistory, setUploadHistory] = useState<UploadLogItem[]>([]);
const [dragging, setDragging] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const resetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
fetchUploadLogs(10)
.then(setUploadHistory)
.catch(err => console.error('[AssetUploadPanel] 이력 로드 실패:', err));
}, []);
useEffect(() => {
return () => {
if (resetTimerRef.current) clearTimeout(resetTimerRef.current);
};
}, []);
const handleFileSelect = (file: File | null) => {
if (!file) return;
const ext = file.name.split('.').pop()?.toLowerCase();
if (ext !== 'xlsx' && ext !== 'csv') return;
setSelectedFile(file);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragging(false);
const file = e.dataTransfer.files[0] ?? null;
handleFileSelect(file);
};
const handleUpload = () => {
if (!selectedFile) return;
setUploaded(true);
resetTimerRef.current = setTimeout(() => {
setUploaded(false);
setSelectedFile(null);
}, 3000);
};
return (
<div className="flex flex-col h-full">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-border flex-shrink-0">
<h1 className="text-lg font-bold text-text-1 font-korean"> </h1>
<p className="text-xs text-text-3 mt-1 font-korean"> </p>
</div>
{/* 본문 */}
<div className="flex-1 overflow-auto p-6">
<div className="flex gap-6 h-full">
{/* 좌측: 파일 업로드 */}
<div className="flex-1 max-w-[560px] space-y-4">
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
</div>
<div className="px-5 py-4 space-y-4">
{/* 드롭존 */}
<div
onDragOver={e => { e.preventDefault(); setDragging(true); }}
onDragLeave={() => setDragging(false)}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`rounded-lg border-2 border-dashed py-8 text-center cursor-pointer transition-colors ${
dragging
? 'border-primary-cyan bg-[rgba(6,182,212,0.05)]'
: 'border-border hover:border-primary-cyan/50 bg-bg-2'
}`}
>
<div className="text-3xl mb-2 opacity-40">📁</div>
{selectedFile ? (
<div className="text-xs font-semibold text-primary-cyan font-korean mb-1">{selectedFile.name}</div>
) : (
<>
<div className="text-xs font-semibold text-text-2 font-korean mb-1"> </div>
<div className="text-[10px] text-text-3 font-korean mb-3">(.xlsx), CSV · 10MB</div>
<button
type="button"
className="px-4 py-1.5 text-xs font-semibold rounded-md bg-primary-cyan text-bg-0
hover:shadow-[0_0_12px_rgba(6,182,212,0.3)] transition-all font-korean"
onClick={e => { e.stopPropagation(); fileInputRef.current?.click(); }}
>
</button>
</>
)}
<input
ref={fileInputRef}
type="file"
accept=".xlsx,.csv"
className="hidden"
onChange={e => handleFileSelect(e.target.files?.[0] ?? null)}
/>
</div>
{/* 자산 분류 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5"> </label>
<select
value={assetCategory}
onChange={e => setAssetCategory(e.target.value)}
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md
text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
>
{ASSET_CATEGORIES.map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
</div>
{/* 대상 관할 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5"> </label>
<select
value={jurisdiction}
onChange={e => setJurisdiction(e.target.value)}
className="w-full px-3 py-2 text-xs bg-bg-2 border border-border rounded-md
text-text-1 focus:border-primary-cyan focus:outline-none font-korean"
>
{JURISDICTIONS.map(j => (
<option key={j} value={j}>{j}</option>
))}
</select>
</div>
{/* 업로드 방식 */}
<div>
<label className="block text-[11px] font-semibold text-text-2 font-korean mb-1.5"> </label>
<div className="flex gap-4">
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-text-2 font-korean">
<input
type="radio"
checked={uploadMode === 'add'}
onChange={() => setUploadMode('add')}
className="accent-primary-cyan"
/>
( + )
</label>
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-text-2 font-korean">
<input
type="radio"
checked={uploadMode === 'replace'}
onChange={() => setUploadMode('replace')}
className="accent-primary-cyan"
/>
( )
</label>
</div>
</div>
{/* 업로드 버튼 */}
<button
type="button"
onClick={handleUpload}
disabled={!selectedFile || uploaded}
className={`w-full py-2.5 text-xs font-semibold rounded-md transition-all font-korean disabled:opacity-50 ${
uploaded
? 'bg-[rgba(34,197,94,0.15)] text-status-green border border-status-green/30'
: 'bg-primary-cyan text-bg-0 hover:shadow-[0_0_12px_rgba(6,182,212,0.3)]'
}`}
>
{uploaded ? '✅ 업로드 완료!' : '📤 업로드 실행'}
</button>
</div>
</div>
</div>
{/* 우측 */}
<div className="w-[400px] space-y-4 flex-shrink-0">
{/* 수정 권한 체계 */}
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
</div>
<div className="px-5 py-4 space-y-2">
{PERM_ITEMS.map(p => (
<div
key={p.role}
className="flex items-center gap-3 px-4 py-3 bg-bg-2 border border-border rounded-md"
>
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-sm flex-shrink-0"
style={{ background: p.bg }}
>
{p.icon}
</div>
<div>
<div className={`text-xs font-bold font-korean ${p.color}`}>{p.role}</div>
<div className="text-[10px] text-text-3 font-korean mt-0.5">{p.desc}</div>
</div>
</div>
))}
</div>
</div>
{/* 최근 업로드 이력 */}
<div className="rounded-lg border border-border bg-bg-1 overflow-hidden">
<div className="px-5 py-3 border-b border-border">
<h2 className="text-sm font-bold text-text-1 font-korean"> </h2>
</div>
<div className="px-5 py-4 space-y-2">
{uploadHistory.length === 0 ? (
<div className="text-[11px] text-text-3 font-korean text-center py-4"> .</div>
) : uploadHistory.map(h => (
<div
key={h.logSn}
className="flex justify-between items-center px-4 py-3 bg-bg-2 border border-border rounded-md"
>
<div>
<div className="text-xs font-semibold text-text-1 font-korean">{h.fileNm}</div>
<div className="text-[10px] text-text-3 mt-0.5 font-korean">
{formatDate(h.regDtm)} · {h.uploaderNm} · {h.uploadCnt.toLocaleString()}
</div>
</div>
<span className="px-2 py-0.5 rounded-full text-[10px] font-semibold
bg-[rgba(34,197,94,0.15)] text-status-green flex-shrink-0">
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default AssetUploadPanel;