refactor: 모달 Content 컴포넌트 분리 및 파일 입력 UI 공통화
- 모달 컴포넌트에서 Content 분리하여 재사용성 향상 - EstimateDocumentContent, DirectConstructionContent 등 - WorkLogContent, QuotePreviewContent, ReceivingReceiptContent - 파일 입력 공통 UI 컴포넌트 추가 - file-dropzone, file-input, file-list, image-upload - 폼 컴포넌트 코드 정리 및 중복 제거 (-4,056줄) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { Mic, Upload, X, FileText, ExternalLink } from 'lucide-react';
|
||||
import { Mic } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CurrencyInput } from '@/components/ui/currency-input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { FileDropzone } from '@/components/ui/file-dropzone';
|
||||
import { FileList, type NewFile, type ExistingFile } from '@/components/ui/file-list';
|
||||
import type { ProposalData, UploadedFile } from './types';
|
||||
|
||||
interface ProposalFormProps {
|
||||
@@ -15,13 +16,8 @@ interface ProposalFormProps {
|
||||
}
|
||||
|
||||
export function ProposalForm({ data, onChange }: ProposalFormProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files) {
|
||||
onChange({ ...data, attachments: [...data.attachments, ...Array.from(files)] });
|
||||
}
|
||||
const handleFilesSelect = (files: File[]) => {
|
||||
onChange({ ...data, attachments: [...data.attachments, ...files] });
|
||||
};
|
||||
|
||||
// 기존 업로드 파일 삭제
|
||||
@@ -149,113 +145,31 @@ export function ProposalForm({ data, onChange }: ProposalFormProps) {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>첨부파일</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
파일 추가
|
||||
</Button>
|
||||
</div>
|
||||
<FileDropzone
|
||||
onFilesSelect={handleFilesSelect}
|
||||
multiple
|
||||
accept="image/*"
|
||||
maxSize={10}
|
||||
compact
|
||||
title="클릭하거나 파일을 드래그하세요"
|
||||
description="이미지 파일만 업로드 가능합니다"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 기존 업로드된 파일 목록 */}
|
||||
{data.uploadedFiles && data.uploadedFiles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-gray-600">저장된 파일</Label>
|
||||
<div className="space-y-2">
|
||||
{data.uploadedFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between gap-2 p-2 bg-gray-50 rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<FileText className="w-4 h-4 text-gray-500 flex-shrink-0" />
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
{file.size && (
|
||||
<span className="text-xs text-gray-400 flex-shrink-0">
|
||||
({Math.round(file.size / 1024)}KB)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{file.url && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => window.open(file.url, '_blank')}
|
||||
title="파일 보기"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveUploadedFile(file.id)}
|
||||
title="파일 삭제"
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 새로 추가할 파일 목록 */}
|
||||
{data.attachments.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm text-gray-600">새로 추가할 파일</Label>
|
||||
<div className="space-y-2">
|
||||
{data.attachments.map((file, index) => (
|
||||
<div
|
||||
key={`new-${index}`}
|
||||
className="flex items-center justify-between gap-2 p-2 bg-blue-50 rounded-md"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<FileText className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
||||
<span className="text-sm truncate">{file.name}</span>
|
||||
<span className="text-xs text-gray-400 flex-shrink-0">
|
||||
({Math.round(file.size / 1024)}KB)
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveAttachment(index)}
|
||||
title="파일 삭제"
|
||||
className="text-red-500 hover:text-red-700 flex-shrink-0"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 파일이 없을 때 안내 메시지 */}
|
||||
{(!data.uploadedFiles || data.uploadedFiles.length === 0) && data.attachments.length === 0 && (
|
||||
<div className="text-sm text-gray-500 text-center py-4 border border-dashed rounded-md">
|
||||
첨부된 파일이 없습니다. 위 버튼을 클릭하여 파일을 추가하세요.
|
||||
</div>
|
||||
)}
|
||||
{/* 파일 목록 */}
|
||||
<FileList
|
||||
files={data.attachments.map((file): NewFile => ({ file }))}
|
||||
existingFiles={(data.uploadedFiles || []).map((file): ExistingFile => ({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
size: file.size,
|
||||
}))}
|
||||
onRemove={handleRemoveAttachment}
|
||||
onRemoveExisting={(id) => handleRemoveUploadedFile(id as number)}
|
||||
emptyMessage="첨부된 파일이 없습니다"
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user