주요 기능: - 품목 CRUD 기능 (생성, 조회, 수정) - 품목 마스터 데이터 관리 시스템 - BOM(Bill of Materials) 관리 기능 - 도면 캔버스 기능 - 품목 속성 및 카테고리 관리 - 스크린 인쇄 생산 관리 페이지 기술 개선: - localStorage SSR 호환성 수정 (9개 useState 초기화) - Shadcn UI 컴포넌트 추가 (table, tabs, alert, drawer 등) - DataContext 및 DeveloperModeContext 추가 - API 라우트 구현 (items, master-data) - 타입 정의 및 유틸리티 함수 추가 빌드 테스트: ✅ 성공 (3.1초) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
211 lines
5.7 KiB
TypeScript
211 lines
5.7 KiB
TypeScript
/**
|
|
* 파일 업로드 컴포넌트
|
|
*
|
|
* 시방서, 인정서, 전개도 등 파일 업로드 UI
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useState, useRef } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { X, Upload, FileText, CheckCircle2 } from 'lucide-react';
|
|
|
|
interface FileUploadProps {
|
|
label: string;
|
|
accept?: string;
|
|
maxSize?: number; // MB
|
|
currentFile?: {
|
|
url: string;
|
|
filename: string;
|
|
};
|
|
onFileSelect: (file: File) => void;
|
|
onFileRemove?: () => void;
|
|
disabled?: boolean;
|
|
required?: boolean;
|
|
}
|
|
|
|
export default function FileUpload({
|
|
label,
|
|
accept = '*/*',
|
|
maxSize = 10, // 10MB default
|
|
currentFile,
|
|
onFileSelect,
|
|
onFileRemove,
|
|
disabled = false,
|
|
required = false,
|
|
}: FileUploadProps) {
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const handleFileChange = (file: File | null) => {
|
|
if (!file) {
|
|
setSelectedFile(null);
|
|
setError(null);
|
|
return;
|
|
}
|
|
|
|
// 파일 크기 검증
|
|
const fileSizeMB = file.size / (1024 * 1024);
|
|
if (fileSizeMB > maxSize) {
|
|
setError(`파일 크기는 ${maxSize}MB 이하여야 합니다`);
|
|
return;
|
|
}
|
|
|
|
setError(null);
|
|
setSelectedFile(file);
|
|
onFileSelect(file);
|
|
};
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0] || null;
|
|
handleFileChange(file);
|
|
};
|
|
|
|
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 = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setIsDragging(false);
|
|
|
|
const file = e.dataTransfer.files?.[0] || null;
|
|
handleFileChange(file);
|
|
};
|
|
|
|
const handleRemove = () => {
|
|
setSelectedFile(null);
|
|
setError(null);
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = '';
|
|
}
|
|
onFileRemove?.();
|
|
};
|
|
|
|
const handleClick = () => {
|
|
fileInputRef.current?.click();
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`file-upload-${label}`}>
|
|
{label}
|
|
{required && <span className="text-red-500 ml-1">*</span>}
|
|
</Label>
|
|
|
|
{/* 파일 입력 (숨김) */}
|
|
<Input
|
|
ref={fileInputRef}
|
|
id={`file-upload-${label}`}
|
|
type="file"
|
|
accept={accept}
|
|
onChange={handleInputChange}
|
|
disabled={disabled}
|
|
className="hidden"
|
|
/>
|
|
|
|
{/* 드래그 앤 드롭 영역 */}
|
|
<div
|
|
onDragEnter={handleDragEnter}
|
|
onDragLeave={handleDragLeave}
|
|
onDragOver={handleDragOver}
|
|
onDrop={handleDrop}
|
|
onClick={handleClick}
|
|
className={`
|
|
border-2 border-dashed rounded-lg p-6
|
|
flex flex-col items-center justify-center
|
|
cursor-pointer transition-colors
|
|
${isDragging ? 'border-primary bg-primary/5' : 'border-gray-300 hover:border-primary/50'}
|
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
`}
|
|
>
|
|
{selectedFile || currentFile ? (
|
|
<div className="w-full">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<FileText className="w-10 h-10 text-primary" />
|
|
<div>
|
|
<p className="font-medium text-sm">
|
|
{selectedFile?.name || currentFile?.filename}
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
{selectedFile
|
|
? `${(selectedFile.size / 1024).toFixed(1)} KB`
|
|
: currentFile?.url && (
|
|
<a
|
|
href={currentFile.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary hover:underline"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
파일 보기
|
|
</a>
|
|
)}
|
|
</p>
|
|
</div>
|
|
{selectedFile && (
|
|
<CheckCircle2 className="w-5 h-5 text-green-500 ml-2" />
|
|
)}
|
|
</div>
|
|
|
|
{!disabled && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleRemove();
|
|
}}
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<Upload className="w-12 h-12 text-gray-400 mb-3" />
|
|
<p className="text-sm text-gray-600 mb-1">
|
|
클릭하거나 파일을 드래그하여 업로드
|
|
</p>
|
|
<p className="text-xs text-gray-500">
|
|
최대 {maxSize}MB
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* 에러 메시지 */}
|
|
{error && (
|
|
<p className="text-sm text-red-500">{error}</p>
|
|
)}
|
|
|
|
{/* 도움말 */}
|
|
{!error && accept !== '*/*' && (
|
|
<p className="text-xs text-gray-500">
|
|
허용 파일 형식: {accept}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
} |