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:
유병철
2026-01-22 15:07:17 +09:00
parent 6fa69d81f4
commit 9464a368ba
48 changed files with 3900 additions and 4063 deletions

View File

@@ -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>