- 모달 컴포넌트에서 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>
177 lines
6.0 KiB
TypeScript
177 lines
6.0 KiB
TypeScript
'use client';
|
|
|
|
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 {
|
|
data: ProposalData;
|
|
onChange: (data: ProposalData) => void;
|
|
}
|
|
|
|
export function ProposalForm({ data, onChange }: ProposalFormProps) {
|
|
const handleFilesSelect = (files: File[]) => {
|
|
onChange({ ...data, attachments: [...data.attachments, ...files] });
|
|
};
|
|
|
|
// 기존 업로드 파일 삭제
|
|
const handleRemoveUploadedFile = (fileId: number) => {
|
|
const updatedFiles = (data.uploadedFiles || []).filter((f) => f.id !== fileId);
|
|
onChange({ ...data, uploadedFiles: updatedFiles });
|
|
};
|
|
|
|
// 새 첨부 파일 삭제
|
|
const handleRemoveAttachment = (index: number) => {
|
|
const updatedAttachments = data.attachments.filter((_, i) => i !== index);
|
|
onChange({ ...data, attachments: updatedAttachments });
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 구매처 정보 */}
|
|
<div className="bg-white rounded-lg border p-6">
|
|
<h3 className="text-lg font-semibold mb-4">구매처 정보</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vendor">구매처</Label>
|
|
<Input
|
|
id="vendor"
|
|
placeholder="구매처를 입력해주세요"
|
|
value={data.vendor}
|
|
onChange={(e) => onChange({ ...data, vendor: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vendorPaymentDate">구매처 결제일</Label>
|
|
<Input
|
|
id="vendorPaymentDate"
|
|
type="date"
|
|
value={data.vendorPaymentDate}
|
|
onChange={(e) => onChange({ ...data, vendorPaymentDate: e.target.value })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 품의서 정보 */}
|
|
<div className="bg-white rounded-lg border p-6">
|
|
<h3 className="text-lg font-semibold mb-4">품의서 정보</h3>
|
|
|
|
<div className="space-y-4">
|
|
{/* 제목 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="title">제목</Label>
|
|
<Input
|
|
id="title"
|
|
placeholder="제목을 입력해주세요"
|
|
value={data.title}
|
|
onChange={(e) => onChange({ ...data, title: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
{/* 품의 내역 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="description">품의 내역</Label>
|
|
<div className="relative">
|
|
<Textarea
|
|
id="description"
|
|
placeholder="품의 내역을 입력해주세요"
|
|
value={data.description}
|
|
onChange={(e) => onChange({ ...data, description: e.target.value })}
|
|
className="min-h-[100px] pr-12"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="absolute right-2 bottom-2"
|
|
title="녹음"
|
|
>
|
|
<Mic className="w-4 h-4" />
|
|
녹음
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 품의 사유 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reason">품의 사유</Label>
|
|
<div className="relative">
|
|
<Textarea
|
|
id="reason"
|
|
placeholder="품의 사유를 입력해주세요"
|
|
value={data.reason}
|
|
onChange={(e) => onChange({ ...data, reason: e.target.value })}
|
|
className="min-h-[100px] pr-12"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="absolute right-2 bottom-2"
|
|
title="녹음"
|
|
>
|
|
<Mic className="w-4 h-4" />
|
|
녹음
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 예상 비용 */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="estimatedCost">예상 비용</Label>
|
|
<CurrencyInput
|
|
id="estimatedCost"
|
|
placeholder="금액을 입력해주세요"
|
|
value={data.estimatedCost || 0}
|
|
onChange={(value) => onChange({ ...data, estimatedCost: value ?? 0 })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 참고 이미지 정보 */}
|
|
<div className="bg-white rounded-lg border p-6">
|
|
<h3 className="text-lg font-semibold mb-4">참고 이미지 정보</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>첨부파일</Label>
|
|
<FileDropzone
|
|
onFilesSelect={handleFilesSelect}
|
|
multiple
|
|
accept="image/*"
|
|
maxSize={10}
|
|
compact
|
|
title="클릭하거나 파일을 드래그하세요"
|
|
description="이미지 파일만 업로드 가능합니다"
|
|
/>
|
|
</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>
|
|
);
|
|
} |