Files
sam-react-prod/src/components/approval/DocumentCreate/ProposalForm.tsx
유병철 9464a368ba 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>
2026-01-22 15:07:17 +09:00

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