- useAccountingListPage, useDateRange 공통 훅 추출 - accounting/shared/ 공통 컴포넌트 분리 - 회계 모듈(입금/출금/매출/매입/청구 등) 중복 로직 통합 - 차량관리 page.tsx 패턴 간소화 - 건설/결재/자재/출하/단가 등 날짜 관련 코드 공통화 - 코드 중복 제거 체크리스트 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
164 lines
6.2 KiB
TypeScript
164 lines
6.2 KiB
TypeScript
'use client';
|
|
|
|
import { Fragment } from 'react';
|
|
import { ContentSkeleton } from '@/components/ui/skeleton';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import type { ExpenseEstimateData, ExpenseEstimateItem } from './types';
|
|
import { formatNumber as formatCurrency } from '@/lib/utils/amount';
|
|
|
|
interface ExpenseEstimateFormProps {
|
|
data: ExpenseEstimateData;
|
|
onChange: (data: ExpenseEstimateData) => void;
|
|
isLoading?: boolean;
|
|
}
|
|
|
|
export function ExpenseEstimateForm({ data, onChange, isLoading }: ExpenseEstimateFormProps) {
|
|
const items = data.items;
|
|
|
|
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;
|
|
|
|
// 로딩 상태
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="bg-white rounded-lg border p-6">
|
|
<h3 className="text-lg font-semibold mb-4">지출 예상 내역서 목록</h3>
|
|
<ContentSkeleton type="table" rows={5} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 빈 상태
|
|
if (items.length === 0) {
|
|
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="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
|
<p>등록된 지출 예상 항목이 없습니다.</p>
|
|
<p className="text-sm mt-1">지출 예상 항목을 먼저 등록해주세요.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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>
|
|
);
|
|
} |