Files
sam-react-prod/src/components/items/FileUpload.tsx
byeongcheolryu 63f5df7d7d feat: 품목 관리 및 마스터 데이터 관리 시스템 구현
주요 기능:
- 품목 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>
2025-11-18 14:17:52 +09:00

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>
);
}