feat: 전자결재 시스템 구현 (기안함, 결재함, 참조함, 문서상세)
- 기안함(DraftBox): 문서 목록, 상신/삭제, 문서작성 연결 - 결재함(ApprovalBox): 결재 대기 문서 목록, 문서상세 모달 연결 - 참조함(ReferenceBox): 참조 문서 목록, 열람/미열람 처리 - 문서작성(DocumentCreate): 품의서, 지출결의서, 지출예상내역서 폼 - 문서상세(DocumentDetail): 공유 모달, 결재선 박스, 3종 문서 뷰어 - 테이블 번호 컬럼 추가 (1번부터 시작) - sonner toast 적용 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
173
src/components/approval/DocumentCreate/ProposalForm.tsx
Normal file
173
src/components/approval/DocumentCreate/ProposalForm.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { Mic, Upload } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { ProposalData } from './types';
|
||||
|
||||
interface ProposalFormProps {
|
||||
data: ProposalData;
|
||||
onChange: (data: ProposalData) => void;
|
||||
}
|
||||
|
||||
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)] });
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<Input
|
||||
id="estimatedCost"
|
||||
type="number"
|
||||
placeholder="금액을 입력해주세요"
|
||||
value={data.estimatedCost || ''}
|
||||
onChange={(e) => onChange({ ...data, estimatedCost: Number(e.target.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-2">
|
||||
<Label>첨부파일</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="text"
|
||||
readOnly
|
||||
placeholder="파일을 선택해주세요"
|
||||
value={data.attachments.map((f) => f.name).join(', ')}
|
||||
className="flex-1"
|
||||
/>
|
||||
<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>
|
||||
{data.attachments.length > 0 && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{data.attachments.length}개 파일 선택됨
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user