324 lines
12 KiB
TypeScript
324 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-color-accent',
|
|
},
|
|
{
|
|
icon: '👁',
|
|
role: '조회자',
|
|
desc: '현황 조회만 가능',
|
|
bg: 'rgba(148,163,184,0.15)',
|
|
color: 'text-fg-sub',
|
|
},
|
|
{
|
|
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-stroke flex-shrink-0">
|
|
<h1 className="text-lg font-bold text-fg font-korean">자산 현행화</h1>
|
|
<p className="text-xs text-fg-disabled 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-stroke bg-bg-surface overflow-hidden">
|
|
<div className="px-5 py-3 border-b border-stroke">
|
|
<h2 className="text-sm font-bold text-fg 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-color-accent bg-[rgba(6,182,212,0.05)]'
|
|
: 'border-stroke hover:border-[rgba(6,182,212,0.5)] bg-bg-elevated'
|
|
}`}
|
|
>
|
|
<div className="text-3xl mb-2 opacity-40">📁</div>
|
|
{selectedFile ? (
|
|
<div className="text-xs font-semibold text-color-accent font-korean mb-1">
|
|
{selectedFile.name}
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="text-xs font-semibold text-fg-sub font-korean mb-1">
|
|
파일을 드래그하거나 클릭하여 업로드
|
|
</div>
|
|
<div className="text-caption text-fg-disabled font-korean mb-3">
|
|
엑셀(.xlsx), CSV 파일 지원 · 최대 10MB
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="px-4 py-1.5 text-xs font-semibold rounded-md bg-color-accent 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-label-2 font-semibold text-fg-sub 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-elevated border border-stroke rounded-md
|
|
text-fg focus:border-color-accent focus:outline-none font-korean"
|
|
>
|
|
{ASSET_CATEGORIES.map((c) => (
|
|
<option key={c} value={c}>
|
|
{c}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* 대상 관할 */}
|
|
<div>
|
|
<label className="block text-label-2 font-semibold text-fg-sub 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-elevated border border-stroke rounded-md
|
|
text-fg focus:border-color-accent focus:outline-none font-korean"
|
|
>
|
|
{JURISDICTIONS.map((j) => (
|
|
<option key={j} value={j}>
|
|
{j}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* 업로드 방식 */}
|
|
<div>
|
|
<label className="block text-label-2 font-semibold text-fg-sub font-korean mb-1.5">
|
|
업로드 방식
|
|
</label>
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center gap-1.5 cursor-pointer text-xs text-fg-sub 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-fg-sub 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-color-success border border-status-green/30'
|
|
: 'bg-color-accent 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-stroke bg-bg-surface overflow-hidden">
|
|
<div className="px-5 py-3 border-b border-stroke">
|
|
<h2 className="text-sm font-bold text-fg 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-elevated border border-stroke 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-caption text-fg-disabled font-korean mt-0.5">
|
|
{p.desc}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 최근 업로드 이력 */}
|
|
<div className="rounded-lg border border-stroke bg-bg-surface overflow-hidden">
|
|
<div className="px-5 py-3 border-b border-stroke">
|
|
<h2 className="text-sm font-bold text-fg font-korean">최근 업로드 이력</h2>
|
|
</div>
|
|
<div className="px-5 py-4 space-y-2">
|
|
{uploadHistory.length === 0 ? (
|
|
<div className="text-label-2 text-fg-disabled 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-elevated border border-stroke rounded-md"
|
|
>
|
|
<div>
|
|
<div className="text-xs font-semibold text-fg font-korean">{h.fileNm}</div>
|
|
<div className="text-caption text-fg-disabled 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-caption font-semibold
|
|
bg-[rgba(34,197,94,0.15)] text-color-success flex-shrink-0"
|
|
>
|
|
완료
|
|
</span>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default AssetUploadPanel;
|