diff --git a/src/components/accounting/BadDebtCollection/actions.ts b/src/components/accounting/BadDebtCollection/actions.ts
index c90cb9c0..3dd07fe6 100644
--- a/src/components/accounting/BadDebtCollection/actions.ts
+++ b/src/components/accounting/BadDebtCollection/actions.ts
@@ -72,8 +72,9 @@ function mapApiStatusToFrontend(apiStatus: string): CollectionStatus {
switch (apiStatus) {
case 'collecting': return 'collecting';
case 'legal_action': return 'legalAction';
- case 'recovered': return 'recovered';
- case 'bad_debt': return 'badDebt';
+ case 'recovered':
+ case 'bad_debt':
+ case 'collection_end': return 'collectionEnd';
default: return 'collecting';
}
}
@@ -82,8 +83,7 @@ function mapFrontendStatusToApi(status: CollectionStatus): string {
switch (status) {
case 'collecting': return 'collecting';
case 'legalAction': return 'legal_action';
- case 'recovered': return 'recovered';
- case 'badDebt': return 'bad_debt';
+ case 'collectionEnd': return 'collection_end';
default: return 'collecting';
}
}
diff --git a/src/components/accounting/BadDebtCollection/index.tsx b/src/components/accounting/BadDebtCollection/index.tsx
index 0e599edb..95d5e297 100644
--- a/src/components/accounting/BadDebtCollection/index.tsx
+++ b/src/components/accounting/BadDebtCollection/index.tsx
@@ -15,7 +15,8 @@ export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2';
import { useState, useMemo, useCallback, useTransition } from 'react';
import { useRouter } from 'next/navigation';
-import { AlertTriangle } from 'lucide-react';
+import { AlertTriangle, Pencil, Trash2 } from 'lucide-react';
+import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
@@ -56,6 +57,7 @@ const tableColumns = [
{ key: 'managerName', label: '담당자', className: 'w-[100px]', sortable: true },
{ key: 'status', label: '상태', className: 'text-center w-[100px]', sortable: true },
{ key: 'setting', label: '설정', className: 'text-center w-[80px]' },
+ { key: 'actions', label: '작업', className: 'text-center w-[80px]' },
];
// ===== Props 타입 정의 =====
@@ -65,8 +67,7 @@ interface BadDebtCollectionProps {
total_amount: number;
collecting_amount: number;
legal_action_amount: number;
- recovered_amount: number;
- bad_debt_amount: number;
+ collection_end_amount: number;
} | null;
}
@@ -132,7 +133,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
totalAmount: initialSummary.total_amount,
collectingAmount: initialSummary.collecting_amount,
legalActionAmount: initialSummary.legal_action_amount,
- recoveredAmount: initialSummary.recovered_amount,
+ collectionEndAmount: initialSummary.collection_end_amount,
};
}
@@ -144,11 +145,11 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
const legalActionAmount = data
.filter((d) => d.status === 'legalAction')
.reduce((sum, d) => sum + d.debtAmount, 0);
- const recoveredAmount = data
- .filter((d) => d.status === 'recovered')
+ const collectionEndAmount = data
+ .filter((d) => d.status === 'collectionEnd')
.reduce((sum, d) => sum + d.debtAmount, 0);
- return { totalAmount, collectingAmount, legalActionAmount, recoveredAmount };
+ return { totalAmount, collectingAmount, legalActionAmount, collectionEndAmount };
}, [data, initialSummary]);
// ===== UniversalListPage Config =====
@@ -335,7 +336,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
},
{
label: '회수완료',
- value: `${formatNumber(statsData.recoveredAmount)}원`,
+ value: `${formatNumber(statsData.collectionEndAmount)}원`,
icon: AlertTriangle,
iconColor: 'text-green-500',
},
@@ -390,6 +391,27 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
disabled={isPending}
/>
+ {/* 작업 */}
+
e.stopPropagation()}>
+
+
+
+
+
),
diff --git a/src/components/accounting/BadDebtCollection/types.ts b/src/components/accounting/BadDebtCollection/types.ts
index 3d4c83bb..d46b81e1 100644
--- a/src/components/accounting/BadDebtCollection/types.ts
+++ b/src/components/accounting/BadDebtCollection/types.ts
@@ -1,7 +1,15 @@
// ===== 악성채권 추심관리 타입 정의 =====
// 추심 상태
-export type CollectionStatus = 'collecting' | 'legalAction' | 'recovered' | 'badDebt';
+export type CollectionStatus = 'collecting' | 'legalAction' | 'collectionEnd';
+
+// 추심종료 사유
+export type CollectionEndReason = 'recovered' | 'badDebt';
+
+export const COLLECTION_END_REASON_OPTIONS: { value: CollectionEndReason; label: string }[] = [
+ { value: 'recovered', label: '회수완료' },
+ { value: 'badDebt', label: '대손처리' },
+];
// 정렬 옵션
export type SortOption = 'latest' | 'oldest';
@@ -70,6 +78,7 @@ export interface BadDebtRecord {
debtAmount: number; // 총 미수금액
badDebtCount: number; // 악성채권 건수
status: CollectionStatus; // 대표 상태 (가장 최근)
+ collectionEndReason?: CollectionEndReason; // 추심종료 사유 (status === 'collectionEnd'일 때)
overdueDays: number; // 최대 연체일수
overdueToggle: boolean;
occurrenceDate: string;
diff --git a/src/components/accounting/DailyReport/index.tsx b/src/components/accounting/DailyReport/index.tsx
index b1bd44cb..555889e0 100644
--- a/src/components/accounting/DailyReport/index.tsx
+++ b/src/components/accounting/DailyReport/index.tsx
@@ -1,9 +1,9 @@
'use client';
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
-import { format, parseISO } from 'date-fns';
+import { format, parseISO, subMonths, startOfMonth, endOfMonth } from 'date-fns';
import { ko } from 'date-fns/locale';
-import { Download, FileText, Loader2, RefreshCw, Calendar } from 'lucide-react';
+import { Download, FileText, Loader2, Printer, Search } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
@@ -15,18 +15,27 @@ import {
TableRow,
TableFooter,
} from '@/components/ui/table';
-import { DatePicker } from '@/components/ui/date-picker';
+import { DateRangePicker } from '@/components/ui/date-range-picker';
+import { Input } from '@/components/ui/input';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { formatNumber as formatAmount } from '@/lib/utils/amount';
-import { Badge } from '@/components/ui/badge';
import type { NoteReceivableItem, DailyAccountItem } from './types';
-import { MATCH_STATUS_LABELS, MATCH_STATUS_COLORS } from './types';
import { getNoteReceivables, getDailyAccounts, getDailyReportSummary } from './actions';
import { toast } from 'sonner';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { usePermission } from '@/hooks/usePermission';
+// ===== 빠른 월 선택 버튼 정의 =====
+const QUICK_MONTH_BUTTONS = [
+ { label: '이번달', months: 0 },
+ { label: '지난달', months: 1 },
+ { label: 'D-2월', months: 2 },
+ { label: 'D-3월', months: 3 },
+ { label: 'D-4월', months: 4 },
+ { label: 'D-5월', months: 5 },
+] as const;
+
// ===== Props 인터페이스 =====
interface DailyReportProps {
initialNoteReceivables?: NoteReceivableItem[];
@@ -36,7 +45,9 @@ interface DailyReportProps {
export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts = [] }: DailyReportProps) {
const { canExport } = usePermission();
// ===== 상태 관리 =====
- const [selectedDate, setSelectedDate] = useState(() => format(new Date(), 'yyyy-MM-dd'));
+ const [startDate, setStartDate] = useState(() => format(startOfMonth(new Date()), 'yyyy-MM-dd'));
+ const [endDate, setEndDate] = useState(() => format(new Date(), 'yyyy-MM-dd'));
+ const [searchTerm, setSearchTerm] = useState('');
const [noteReceivables, setNoteReceivables] = useState
(initialNoteReceivables);
const [dailyAccounts, setDailyAccounts] = useState(initialDailyAccounts);
const [summary, setSummary] = useState<{
@@ -53,9 +64,9 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
setIsLoading(true);
try {
const [noteResult, accountResult, summaryResult] = await Promise.all([
- getNoteReceivables({ date: selectedDate }),
- getDailyAccounts({ date: selectedDate }),
- getDailyReportSummary({ date: selectedDate }),
+ getNoteReceivables({ date: startDate }),
+ getDailyAccounts({ date: startDate }),
+ getDailyReportSummary({ date: startDate }),
]);
if (noteResult.success) {
@@ -81,20 +92,20 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
} finally {
setIsLoading(false);
}
- }, [selectedDate]);
+ }, [startDate]);
// ===== 초기 로드 및 날짜 변경시 재로드 =====
const isInitialMount = useRef(true);
- const prevDateRef = useRef(selectedDate);
+ const prevDateRef = useRef(startDate);
useEffect(() => {
// 초기 마운트 또는 날짜가 실제로 변경된 경우에만 로드
- if (isInitialMount.current || prevDateRef.current !== selectedDate) {
+ if (isInitialMount.current || prevDateRef.current !== startDate) {
isInitialMount.current = false;
- prevDateRef.current = selectedDate;
+ prevDateRef.current = startDate;
loadData();
}
- }, [selectedDate, loadData]);
+ }, [startDate, loadData]);
// ===== 어음 합계 (API 요약 사용) =====
const noteReceivableTotal = useMemo(() => {
@@ -144,9 +155,9 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
}, [accountTotals]);
// ===== 선택된 날짜 정보 =====
- const selectedDateInfo = useMemo(() => {
+ const startDateInfo = useMemo(() => {
try {
- const date = parseISO(selectedDate);
+ const date = parseISO(startDate);
return {
formatted: format(date, 'yyyy년 M월 d일', { locale: ko }),
dayOfWeek: format(date, 'EEEE', { locale: ko }),
@@ -154,12 +165,12 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
} catch {
return { formatted: '', dayOfWeek: '' };
}
- }, [selectedDate]);
+ }, [startDate]);
// ===== 엑셀 다운로드 (프록시 API 직접 호출) =====
const handleExcelDownload = useCallback(async () => {
try {
- const url = `/api/proxy/daily-report/export?date=${selectedDate}`;
+ const url = `/api/proxy/daily-report/export?date=${startDate}`;
const response = await fetch(url);
if (!response.ok) {
@@ -169,7 +180,7 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
const blob = await response.blob();
const contentDisposition = response.headers.get('Content-Disposition');
- const filename = contentDisposition?.match(/filename="?(.+)"?/)?.[1] || `일일일보_${selectedDate}.xlsx`;
+ const filename = contentDisposition?.match(/filename="?(.+)"?/)?.[1] || `일일일보_${startDate}.xlsx`;
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -183,7 +194,36 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
} catch {
toast.error('엑셀 다운로드 중 오류가 발생했습니다.');
}
- }, [selectedDate]);
+ }, [startDate]);
+
+ // ===== 빠른 월 선택 =====
+ const handleQuickMonth = useCallback((monthsAgo: number) => {
+ const target = monthsAgo === 0 ? new Date() : subMonths(new Date(), monthsAgo);
+ setStartDate(format(startOfMonth(target), 'yyyy-MM-dd'));
+ setEndDate(format(endOfMonth(target), 'yyyy-MM-dd'));
+ }, []);
+
+ // ===== 인쇄 =====
+ const handlePrint = useCallback(() => {
+ window.print();
+ }, []);
+
+ // ===== 검색 필터링 =====
+ const filteredNoteReceivables = useMemo(() => {
+ if (!searchTerm) return noteReceivables;
+ const term = searchTerm.toLowerCase();
+ return noteReceivables.filter(item =>
+ item.content.toLowerCase().includes(term)
+ );
+ }, [noteReceivables, searchTerm]);
+
+ const filteredDailyAccounts = useMemo(() => {
+ if (!searchTerm) return dailyAccounts;
+ const term = searchTerm.toLowerCase();
+ return dailyAccounts.filter(item =>
+ item.category.toLowerCase().includes(term)
+ );
+ }, [dailyAccounts, searchTerm]);
return (
@@ -194,42 +234,57 @@ export function DailyReport({ initialNoteReceivables = [], initialDailyAccounts
icon={FileText}
/>
- {/* 헤더 액션 (날짜 선택, 버튼 등) */}
+ {/* 헤더 액션 (날짜 선택, 빠른 월 선택, 검색, 인쇄) */}
-
-
-
-
- 조회 일자
-
-
-
-
- {canExport && (
-