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:
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { ApprovalPerson } from './types';
|
||||
import { MOCK_EMPLOYEES } from './types';
|
||||
|
||||
interface ApprovalLineSectionProps {
|
||||
data: ApprovalPerson[];
|
||||
onChange: (data: ApprovalPerson[]) => void;
|
||||
}
|
||||
|
||||
export function ApprovalLineSection({ data, onChange }: ApprovalLineSectionProps) {
|
||||
const handleAdd = () => {
|
||||
const newPerson: ApprovalPerson = {
|
||||
id: `temp-${Date.now()}`,
|
||||
department: '',
|
||||
position: '',
|
||||
name: '',
|
||||
};
|
||||
onChange([...data, newPerson]);
|
||||
};
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
onChange(data.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleChange = (index: number, employeeId: string) => {
|
||||
const employee = MOCK_EMPLOYEES.find((e) => e.id === employeeId);
|
||||
if (employee) {
|
||||
const newData = [...data];
|
||||
newData[index] = { ...employee };
|
||||
onChange(newData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">결재선</h3>
|
||||
<Button variant="outline" size="sm" onClick={handleAdd}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-500 mb-2">부서 / 직책 / 이름</div>
|
||||
|
||||
{data.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-400">
|
||||
결재선을 추가해주세요
|
||||
</div>
|
||||
) : (
|
||||
data.map((person, index) => (
|
||||
<div key={person.id} className="flex items-center gap-2">
|
||||
<span className="w-8 text-center text-sm text-gray-500">{index + 1}</span>
|
||||
<Select
|
||||
value={person.id.startsWith('temp-') ? '' : person.id}
|
||||
onValueChange={(value) => handleChange(index, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MOCK_EMPLOYEES.map((employee) => (
|
||||
<SelectItem key={employee.id} value={employee.id}>
|
||||
{employee.department} / {employee.position} / {employee.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleRemove(index)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
src/components/approval/DocumentCreate/BasicInfoSection.tsx
Normal file
81
src/components/approval/DocumentCreate/BasicInfoSection.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { BasicInfo, DocumentType } from './types';
|
||||
import { DOCUMENT_TYPE_OPTIONS } from './types';
|
||||
|
||||
interface BasicInfoSectionProps {
|
||||
data: BasicInfo;
|
||||
onChange: (data: BasicInfo) => void;
|
||||
}
|
||||
|
||||
export function BasicInfoSection({ data, onChange }: BasicInfoSectionProps) {
|
||||
return (
|
||||
<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="drafter">기안자</Label>
|
||||
<Input
|
||||
id="drafter"
|
||||
value={data.drafter}
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 작성일 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="draftDate">작성일</Label>
|
||||
<Input
|
||||
id="draftDate"
|
||||
value={data.draftDate}
|
||||
disabled
|
||||
className="bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 문서번호 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="documentNo">문서번호</Label>
|
||||
<Input
|
||||
id="documentNo"
|
||||
placeholder="문서번호를 입력해주세요"
|
||||
value={data.documentNo}
|
||||
onChange={(e) => onChange({ ...data, documentNo: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 문서유형 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="documentType">문서유형</Label>
|
||||
<Select
|
||||
value={data.documentType}
|
||||
onValueChange={(value) => onChange({ ...data, documentType: value as DocumentType })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="문서유형 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DOCUMENT_TYPE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
152
src/components/approval/DocumentCreate/ExpenseEstimateForm.tsx
Normal file
152
src/components/approval/DocumentCreate/ExpenseEstimateForm.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment } from 'react';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { ExpenseEstimateData, ExpenseEstimateItem } from './types';
|
||||
|
||||
interface ExpenseEstimateFormProps {
|
||||
data: ExpenseEstimateData;
|
||||
onChange: (data: ExpenseEstimateData) => void;
|
||||
}
|
||||
|
||||
// Mock 데이터 생성
|
||||
const generateMockEstimateItems = (): ExpenseEstimateItem[] => {
|
||||
return [
|
||||
{ id: '1', checked: false, expectedPaymentDate: '2025-11-12', category: '통신 서비스', amount: 1000000, vendor: '회사명', memo: '국민 1234 홍길동' },
|
||||
{ id: '2', checked: false, expectedPaymentDate: '2025-11-12', category: '인건 대행', amount: 1000000, vendor: '회사명', memo: '국민 1234 홍길동' },
|
||||
{ id: '3', checked: false, expectedPaymentDate: '2025-11-12', category: '통신 서비스', amount: 1000000, vendor: '회사명', memo: '국민 1234 홍길동' },
|
||||
{ id: '4', checked: false, expectedPaymentDate: '2025-11-12', category: '인건 대행', amount: 1000000, vendor: '회사명', memo: '국민 1234 홍길동' },
|
||||
// 11월 소계 후
|
||||
{ id: '5', checked: false, expectedPaymentDate: '2025-12-12', category: '기타서비스 12월분', amount: 1000000, vendor: '회사명', memo: '국민 1234 홍길동' },
|
||||
{ id: '6', checked: false, expectedPaymentDate: '2025-12-12', category: '통신 서비스', amount: 1000000, vendor: '회사명', memo: '국민 1234 홍길동' },
|
||||
];
|
||||
};
|
||||
|
||||
export function ExpenseEstimateForm({ data, onChange }: ExpenseEstimateFormProps) {
|
||||
// Mock 데이터 초기화
|
||||
const items = data.items.length > 0 ? data.items : generateMockEstimateItems();
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
};
|
||||
|
||||
const handleCheckChange = (id: string, checked: boolean) => {
|
||||
const newItems = items.map((item) =>
|
||||
item.id === id ? { ...item, checked } : item
|
||||
);
|
||||
const totalExpense = newItems.reduce((sum, item) => sum + item.amount, 0);
|
||||
onChange({
|
||||
...data,
|
||||
items: newItems,
|
||||
totalExpense,
|
||||
finalDifference: data.accountBalance - totalExpense,
|
||||
});
|
||||
};
|
||||
|
||||
// 월별 그룹핑
|
||||
const groupedByMonth = items.reduce((acc, item) => {
|
||||
const month = item.expectedPaymentDate.substring(0, 7); // YYYY-MM
|
||||
if (!acc[month]) {
|
||||
acc[month] = [];
|
||||
}
|
||||
acc[month].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, ExpenseEstimateItem[]>);
|
||||
|
||||
const getMonthSubtotal = (monthItems: ExpenseEstimateItem[]) => {
|
||||
return monthItems.reduce((sum, item) => sum + item.amount, 0);
|
||||
};
|
||||
|
||||
const totalExpense = items.reduce((sum, item) => sum + item.amount, 0);
|
||||
const accountBalance = data.accountBalance || 10000000; // Mock 계좌 잔액
|
||||
const finalDifference = accountBalance - totalExpense;
|
||||
|
||||
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="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[50px] text-center"></TableHead>
|
||||
<TableHead className="min-w-[120px]">예상 지급일</TableHead>
|
||||
<TableHead className="min-w-[150px]">항목</TableHead>
|
||||
<TableHead className="min-w-[120px] text-right">지출금액</TableHead>
|
||||
<TableHead className="min-w-[100px]">거래처</TableHead>
|
||||
<TableHead className="min-w-[150px]">적록</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{Object.entries(groupedByMonth).map(([month, monthItems]) => (
|
||||
<Fragment key={month}>
|
||||
{monthItems.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
checked={item.checked}
|
||||
onCheckedChange={(checked) => handleCheckChange(item.id, !!checked)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{item.expectedPaymentDate}</TableCell>
|
||||
<TableCell>{item.category}</TableCell>
|
||||
<TableCell className="text-right text-blue-600 font-medium">
|
||||
{formatCurrency(item.amount)}
|
||||
</TableCell>
|
||||
<TableCell>{item.vendor}</TableCell>
|
||||
<TableCell>{item.memo}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{/* 월별 소계 */}
|
||||
<TableRow className="bg-pink-50">
|
||||
<TableCell colSpan={2} className="font-medium">
|
||||
{month.replace('-', '년 ')}월 계
|
||||
</TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell className="text-right text-red-600 font-bold">
|
||||
{formatCurrency(getMonthSubtotal(monthItems))}
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{/* 합계 행들 */}
|
||||
<TableRow className="bg-gray-50 border-t-2">
|
||||
<TableCell colSpan={3} className="font-semibold">지출 합계</TableCell>
|
||||
<TableCell className="text-right text-red-600 font-bold">
|
||||
{formatCurrency(totalExpense)}
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableCell colSpan={3} className="font-semibold">계좌 잔액</TableCell>
|
||||
<TableCell className="text-right font-bold">
|
||||
{formatCurrency(accountBalance)}
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableCell colSpan={3} className="font-semibold">최종 차액</TableCell>
|
||||
<TableCell className={`text-right font-bold ${finalDifference >= 0 ? 'text-blue-600' : 'text-red-600'}`}>
|
||||
{formatCurrency(finalDifference)}
|
||||
</TableCell>
|
||||
<TableCell colSpan={2}></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
242
src/components/approval/DocumentCreate/ExpenseReportForm.tsx
Normal file
242
src/components/approval/DocumentCreate/ExpenseReportForm.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
'use client';
|
||||
|
||||
import { useRef } from 'react';
|
||||
import { Plus, X, Upload } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { ExpenseReportData, ExpenseReportItem } from './types';
|
||||
import { CARD_OPTIONS } from './types';
|
||||
|
||||
interface ExpenseReportFormProps {
|
||||
data: ExpenseReportData;
|
||||
onChange: (data: ExpenseReportData) => void;
|
||||
}
|
||||
|
||||
export function ExpenseReportForm({ data, onChange }: ExpenseReportFormProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleAddItem = () => {
|
||||
const newItem: ExpenseReportItem = {
|
||||
id: `item-${Date.now()}`,
|
||||
description: '',
|
||||
amount: 0,
|
||||
note: '',
|
||||
};
|
||||
onChange({ ...data, items: [...data.items, newItem] });
|
||||
};
|
||||
|
||||
const handleRemoveItem = (index: number) => {
|
||||
const newItems = data.items.filter((_, i) => i !== index);
|
||||
const totalAmount = newItems.reduce((sum, item) => sum + item.amount, 0);
|
||||
onChange({ ...data, items: newItems, totalAmount });
|
||||
};
|
||||
|
||||
const handleItemChange = (index: number, field: keyof ExpenseReportItem, value: string | number) => {
|
||||
const newItems = [...data.items];
|
||||
newItems[index] = { ...newItems[index], [field]: value };
|
||||
const totalAmount = newItems.reduce((sum, item) => sum + item.amount, 0);
|
||||
onChange({ ...data, items: newItems, totalAmount });
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files) {
|
||||
onChange({ ...data, attachments: [...data.attachments, ...Array.from(files)] });
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('ko-KR').format(amount);
|
||||
};
|
||||
|
||||
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="requestDate">지출 요청일</Label>
|
||||
<Input
|
||||
id="requestDate"
|
||||
type="date"
|
||||
value={data.requestDate}
|
||||
onChange={(e) => onChange({ ...data, requestDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="paymentDate">결제일</Label>
|
||||
<Input
|
||||
id="paymentDate"
|
||||
type="date"
|
||||
value={data.paymentDate}
|
||||
onChange={(e) => onChange({ ...data, paymentDate: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 지출결의서 정보 */}
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">지출결의서 정보</h3>
|
||||
<Button variant="outline" size="sm" onClick={handleAddItem}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[60px] text-center">번호</TableHead>
|
||||
<TableHead className="min-w-[200px]">적요</TableHead>
|
||||
<TableHead className="min-w-[150px]">금액</TableHead>
|
||||
<TableHead className="min-w-[150px]">비고</TableHead>
|
||||
<TableHead className="w-[60px] text-center">삭제</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-8 text-gray-400">
|
||||
항목을 추가해주세요
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.items.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell className="text-center">{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
placeholder="적요를 입력해주세요"
|
||||
value={item.description}
|
||||
onChange={(e) => handleItemChange(index, 'description', e.target.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="금액을 입력해주세요"
|
||||
value={item.amount || ''}
|
||||
onChange={(e) => handleItemChange(index, 'amount', Number(e.target.value) || 0)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Input
|
||||
placeholder="비고를 입력해주세요"
|
||||
value={item.note}
|
||||
onChange={(e) => handleItemChange(index, 'note', e.target.value)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleRemoveItem(index)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 결제 정보 */}
|
||||
<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="card">카드</Label>
|
||||
<Select
|
||||
value={data.cardId}
|
||||
onValueChange={(value) => onChange({ ...data, cardId: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="카드를 선택해주세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CARD_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>총 비용</Label>
|
||||
<div className="h-10 px-3 py-2 bg-gray-50 border rounded-md text-right font-semibold">
|
||||
{formatCurrency(data.totalAmount)}원
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
94
src/components/approval/DocumentCreate/ReferenceSection.tsx
Normal file
94
src/components/approval/DocumentCreate/ReferenceSection.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import { Plus, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { ApprovalPerson } from './types';
|
||||
import { MOCK_EMPLOYEES } from './types';
|
||||
|
||||
interface ReferenceSectionProps {
|
||||
data: ApprovalPerson[];
|
||||
onChange: (data: ApprovalPerson[]) => void;
|
||||
}
|
||||
|
||||
export function ReferenceSection({ data, onChange }: ReferenceSectionProps) {
|
||||
const handleAdd = () => {
|
||||
const newPerson: ApprovalPerson = {
|
||||
id: `temp-${Date.now()}`,
|
||||
department: '',
|
||||
position: '',
|
||||
name: '',
|
||||
};
|
||||
onChange([...data, newPerson]);
|
||||
};
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
onChange(data.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleChange = (index: number, employeeId: string) => {
|
||||
const employee = MOCK_EMPLOYEES.find((e) => e.id === employeeId);
|
||||
if (employee) {
|
||||
const newData = [...data];
|
||||
newData[index] = { ...employee };
|
||||
onChange(newData);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">참조</h3>
|
||||
<Button variant="outline" size="sm" onClick={handleAdd}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-500 mb-2">부서 / 직책 / 이름</div>
|
||||
|
||||
{data.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-400">
|
||||
참조자를 추가해주세요
|
||||
</div>
|
||||
) : (
|
||||
data.map((person, index) => (
|
||||
<div key={person.id} className="flex items-center gap-2">
|
||||
<span className="w-8 text-center text-sm text-gray-500">{index + 1}</span>
|
||||
<Select
|
||||
value={person.id.startsWith('temp-') ? '' : person.id}
|
||||
onValueChange={(value) => handleChange(index, value)}
|
||||
>
|
||||
<SelectTrigger className="flex-1">
|
||||
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MOCK_EMPLOYEES.map((employee) => (
|
||||
<SelectItem key={employee.id} value={employee.id}>
|
||||
{employee.department} / {employee.position} / {employee.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleRemove(index)}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
293
src/components/approval/DocumentCreate/index.tsx
Normal file
293
src/components/approval/DocumentCreate/index.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
import { FileText, Trash2, Send, Save, ArrowLeft, Eye } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { BasicInfoSection } from './BasicInfoSection';
|
||||
import { ApprovalLineSection } from './ApprovalLineSection';
|
||||
import { ReferenceSection } from './ReferenceSection';
|
||||
import { ProposalForm } from './ProposalForm';
|
||||
import { ExpenseReportForm } from './ExpenseReportForm';
|
||||
import { ExpenseEstimateForm } from './ExpenseEstimateForm';
|
||||
import { DocumentDetailModal } from '@/components/approval/DocumentDetail';
|
||||
import type {
|
||||
DocumentType as ModalDocumentType,
|
||||
ProposalDocumentData,
|
||||
ExpenseReportDocumentData,
|
||||
ExpenseEstimateDocumentData,
|
||||
} from '@/components/approval/DocumentDetail/types';
|
||||
import type {
|
||||
DocumentType,
|
||||
BasicInfo,
|
||||
ApprovalPerson,
|
||||
ProposalData,
|
||||
ExpenseReportData,
|
||||
ExpenseEstimateData,
|
||||
} from './types';
|
||||
|
||||
// 초기 데이터
|
||||
const getInitialBasicInfo = (): BasicInfo => ({
|
||||
drafter: '홍길동',
|
||||
draftDate: format(new Date(), 'yyyy-MM-dd HH:mm'),
|
||||
documentNo: '',
|
||||
documentType: 'proposal',
|
||||
});
|
||||
|
||||
const getInitialProposalData = (): ProposalData => ({
|
||||
vendor: '',
|
||||
vendorPaymentDate: format(new Date(), 'yyyy-MM-dd'),
|
||||
title: '',
|
||||
description: '',
|
||||
reason: '',
|
||||
estimatedCost: 0,
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
const getInitialExpenseReportData = (): ExpenseReportData => ({
|
||||
requestDate: format(new Date(), 'yyyy-MM-dd'),
|
||||
paymentDate: format(new Date(), 'yyyy-MM-dd'),
|
||||
items: [],
|
||||
cardId: '',
|
||||
totalAmount: 0,
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
const getInitialExpenseEstimateData = (): ExpenseEstimateData => ({
|
||||
items: [],
|
||||
totalExpense: 0,
|
||||
accountBalance: 10000000,
|
||||
finalDifference: 10000000,
|
||||
});
|
||||
|
||||
export function DocumentCreate() {
|
||||
const router = useRouter();
|
||||
|
||||
// 상태 관리
|
||||
const [basicInfo, setBasicInfo] = useState<BasicInfo>(getInitialBasicInfo);
|
||||
const [approvalLine, setApprovalLine] = useState<ApprovalPerson[]>([]);
|
||||
const [references, setReferences] = useState<ApprovalPerson[]>([]);
|
||||
const [proposalData, setProposalData] = useState<ProposalData>(getInitialProposalData);
|
||||
const [expenseReportData, setExpenseReportData] = useState<ExpenseReportData>(getInitialExpenseReportData);
|
||||
const [expenseEstimateData, setExpenseEstimateData] = useState<ExpenseEstimateData>(getInitialExpenseEstimateData);
|
||||
|
||||
// 미리보기 모달 상태
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
|
||||
// 핸들러
|
||||
const handleBack = useCallback(() => {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (confirm('작성 중인 문서를 삭제하시겠습니까?')) {
|
||||
router.back();
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
console.log('상신:', {
|
||||
basicInfo,
|
||||
approvalLine,
|
||||
references,
|
||||
proposalData: basicInfo.documentType === 'proposal' ? proposalData : undefined,
|
||||
expenseReportData: basicInfo.documentType === 'expenseReport' ? expenseReportData : undefined,
|
||||
expenseEstimateData: basicInfo.documentType === 'expenseEstimate' ? expenseEstimateData : undefined,
|
||||
});
|
||||
alert('문서가 상신되었습니다.');
|
||||
router.back();
|
||||
}, [basicInfo, approvalLine, references, proposalData, expenseReportData, expenseEstimateData, router]);
|
||||
|
||||
const handleSaveDraft = useCallback(() => {
|
||||
console.log('임시저장:', {
|
||||
basicInfo,
|
||||
approvalLine,
|
||||
references,
|
||||
proposalData: basicInfo.documentType === 'proposal' ? proposalData : undefined,
|
||||
expenseReportData: basicInfo.documentType === 'expenseReport' ? expenseReportData : undefined,
|
||||
expenseEstimateData: basicInfo.documentType === 'expenseEstimate' ? expenseEstimateData : undefined,
|
||||
});
|
||||
alert('임시저장되었습니다.');
|
||||
}, [basicInfo, approvalLine, references, proposalData, expenseReportData, expenseEstimateData]);
|
||||
|
||||
// 미리보기 핸들러
|
||||
const handlePreview = useCallback(() => {
|
||||
setIsPreviewOpen(true);
|
||||
}, []);
|
||||
|
||||
// 미리보기용 데이터 변환
|
||||
const getPreviewData = useCallback((): ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData => {
|
||||
const drafter = {
|
||||
id: 'drafter-1',
|
||||
name: basicInfo.drafter,
|
||||
position: '사원',
|
||||
department: '개발팀',
|
||||
status: 'approved' as const,
|
||||
};
|
||||
const approvers = approvalLine.map((a, index) => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
position: a.position,
|
||||
department: a.department,
|
||||
status: (index === 0 ? 'pending' : 'none') as 'pending' | 'approved' | 'rejected' | 'none',
|
||||
}));
|
||||
|
||||
switch (basicInfo.documentType) {
|
||||
case 'expenseEstimate':
|
||||
return {
|
||||
documentNo: basicInfo.documentNo || '미발급',
|
||||
createdAt: basicInfo.draftDate,
|
||||
items: expenseEstimateData.items.map(item => ({
|
||||
id: item.id,
|
||||
expectedPaymentDate: item.expectedPaymentDate,
|
||||
category: item.category,
|
||||
amount: item.amount,
|
||||
vendor: item.vendor,
|
||||
account: item.memo || '',
|
||||
})),
|
||||
totalExpense: expenseEstimateData.totalExpense,
|
||||
accountBalance: expenseEstimateData.accountBalance,
|
||||
finalDifference: expenseEstimateData.finalDifference,
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
case 'expenseReport':
|
||||
return {
|
||||
documentNo: basicInfo.documentNo || '미발급',
|
||||
createdAt: basicInfo.draftDate,
|
||||
requestDate: expenseReportData.requestDate,
|
||||
paymentDate: expenseReportData.paymentDate,
|
||||
items: expenseReportData.items.map((item, index) => ({
|
||||
id: item.id,
|
||||
no: index + 1,
|
||||
description: item.description,
|
||||
amount: item.amount,
|
||||
note: item.note,
|
||||
})),
|
||||
cardInfo: expenseReportData.cardId || '-',
|
||||
totalAmount: expenseReportData.totalAmount,
|
||||
attachments: expenseReportData.attachments,
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
documentNo: basicInfo.documentNo || '미발급',
|
||||
createdAt: basicInfo.draftDate,
|
||||
vendor: proposalData.vendor || '-',
|
||||
vendorPaymentDate: proposalData.vendorPaymentDate,
|
||||
title: proposalData.title || '(제목 없음)',
|
||||
description: proposalData.description || '-',
|
||||
reason: proposalData.reason || '-',
|
||||
estimatedCost: proposalData.estimatedCost,
|
||||
attachments: proposalData.attachments,
|
||||
approvers,
|
||||
drafter,
|
||||
};
|
||||
}
|
||||
}, [basicInfo, approvalLine, proposalData, expenseReportData, expenseEstimateData]);
|
||||
|
||||
// 문서 유형별 폼 렌더링
|
||||
const renderDocumentTypeForm = () => {
|
||||
switch (basicInfo.documentType) {
|
||||
case 'proposal':
|
||||
return <ProposalForm data={proposalData} onChange={setProposalData} />;
|
||||
case 'expenseReport':
|
||||
return <ExpenseReportForm data={expenseReportData} onChange={setExpenseReportData} />;
|
||||
case 'expenseEstimate':
|
||||
return <ExpenseEstimateForm data={expenseEstimateData} onChange={setExpenseEstimateData} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 px-4 max-w-4xl">
|
||||
{/* 헤더 */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="icon" onClick={handleBack}>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-6 w-6 text-primary" />
|
||||
<div>
|
||||
<CardTitle>문서 작성</CardTitle>
|
||||
<CardDescription>새로운 문서를 작성합니다</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
{/* 액션 버튼 (스텝) */}
|
||||
<div className="flex items-center justify-center gap-2 mb-6">
|
||||
<Button variant="outline" className="min-w-[80px]" onClick={handlePreview}>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
상세
|
||||
</Button>
|
||||
<Button variant="outline" className="min-w-[80px]" onClick={handleDelete}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
<Button variant="default" className="min-w-[80px]" onClick={handleSubmit}>
|
||||
<Send className="w-4 h-4 mr-1" />
|
||||
상신
|
||||
</Button>
|
||||
<Button variant="secondary" className="min-w-[80px]" onClick={handleSaveDraft}>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
임시저장
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 폼 영역 */}
|
||||
<div className="space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<BasicInfoSection data={basicInfo} onChange={setBasicInfo} />
|
||||
|
||||
{/* 결재선 */}
|
||||
<ApprovalLineSection data={approvalLine} onChange={setApprovalLine} />
|
||||
|
||||
{/* 참조 */}
|
||||
<ReferenceSection data={references} onChange={setReferences} />
|
||||
|
||||
{/* 문서 유형별 폼 */}
|
||||
{renderDocumentTypeForm()}
|
||||
</div>
|
||||
|
||||
{/* 하단 고정 버튼 (모바일) */}
|
||||
<div className="fixed bottom-0 left-0 right-0 p-4 bg-white border-t md:hidden">
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1" onClick={handleDelete}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
삭제
|
||||
</Button>
|
||||
<Button variant="secondary" className="flex-1" onClick={handleSaveDraft}>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
임시저장
|
||||
</Button>
|
||||
<Button variant="default" className="flex-1" onClick={handleSubmit}>
|
||||
<Send className="w-4 h-4 mr-1" />
|
||||
상신
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 모바일 하단 여백 */}
|
||||
<div className="h-20 md:hidden" />
|
||||
|
||||
{/* 미리보기 모달 */}
|
||||
<DocumentDetailModal
|
||||
open={isPreviewOpen}
|
||||
onOpenChange={setIsPreviewOpen}
|
||||
documentType={basicInfo.documentType as ModalDocumentType}
|
||||
data={getPreviewData()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
src/components/approval/DocumentCreate/types.ts
Normal file
100
src/components/approval/DocumentCreate/types.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// ===== 문서 작성 타입 정의 =====
|
||||
|
||||
// 문서 유형
|
||||
export type DocumentType = 'proposal' | 'expenseReport' | 'expenseEstimate';
|
||||
|
||||
export const DOCUMENT_TYPE_OPTIONS: { value: DocumentType; label: string }[] = [
|
||||
{ value: 'proposal', label: '품의서' },
|
||||
{ value: 'expenseReport', label: '지출결의서' },
|
||||
{ value: 'expenseEstimate', label: '지출 예상 내역서' },
|
||||
];
|
||||
|
||||
// 결재자/참조자 정보
|
||||
export interface ApprovalPerson {
|
||||
id: string;
|
||||
department: string;
|
||||
position: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 기본 정보
|
||||
export interface BasicInfo {
|
||||
drafter: string;
|
||||
draftDate: string;
|
||||
documentNo: string;
|
||||
documentType: DocumentType;
|
||||
}
|
||||
|
||||
// 품의서 데이터
|
||||
export interface ProposalData {
|
||||
vendor: string;
|
||||
vendorPaymentDate: string;
|
||||
title: string;
|
||||
description: string;
|
||||
reason: string;
|
||||
estimatedCost: number;
|
||||
attachments: File[];
|
||||
}
|
||||
|
||||
// 지출결의서 항목
|
||||
export interface ExpenseReportItem {
|
||||
id: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
note: string;
|
||||
}
|
||||
|
||||
// 지출결의서 데이터
|
||||
export interface ExpenseReportData {
|
||||
requestDate: string;
|
||||
paymentDate: string;
|
||||
items: ExpenseReportItem[];
|
||||
cardId: string;
|
||||
totalAmount: number;
|
||||
attachments: File[];
|
||||
}
|
||||
|
||||
// 지출 예상 내역서 항목
|
||||
export interface ExpenseEstimateItem {
|
||||
id: string;
|
||||
checked: boolean;
|
||||
expectedPaymentDate: string;
|
||||
category: string;
|
||||
amount: number;
|
||||
vendor: string;
|
||||
memo: string;
|
||||
}
|
||||
|
||||
// 지출 예상 내역서 데이터
|
||||
export interface ExpenseEstimateData {
|
||||
items: ExpenseEstimateItem[];
|
||||
totalExpense: number;
|
||||
accountBalance: number;
|
||||
finalDifference: number;
|
||||
}
|
||||
|
||||
// 전체 문서 데이터
|
||||
export interface DocumentFormData {
|
||||
basicInfo: BasicInfo;
|
||||
approvalLine: ApprovalPerson[];
|
||||
references: ApprovalPerson[];
|
||||
proposalData?: ProposalData;
|
||||
expenseReportData?: ExpenseReportData;
|
||||
expenseEstimateData?: ExpenseEstimateData;
|
||||
}
|
||||
|
||||
// 카드 옵션
|
||||
export const CARD_OPTIONS = [
|
||||
{ value: 'ibk-1234', label: 'IBK기업카드_1234 (카드명)' },
|
||||
{ value: 'shinhan-5678', label: '신한카드_5678 (카드명)' },
|
||||
{ value: 'kb-9012', label: 'KB국민카드_9012 (카드명)' },
|
||||
];
|
||||
|
||||
// Mock 사원 데이터
|
||||
export const MOCK_EMPLOYEES: ApprovalPerson[] = [
|
||||
{ id: '1', department: '개발팀', position: '팀장', name: '김철수' },
|
||||
{ id: '2', department: '개발팀', position: '부장', name: '이영희' },
|
||||
{ id: '3', department: '인사팀', position: '팀장', name: '박민수' },
|
||||
{ id: '4', department: '경영지원팀', position: '이사', name: '정수진' },
|
||||
{ id: '5', department: '영업팀', position: '대표', name: '최동현' },
|
||||
];
|
||||
Reference in New Issue
Block a user