diff --git a/src/components/accounting/condolence-expenses/CondolenceExpenseForm.tsx b/src/components/accounting/condolence-expenses/CondolenceExpenseForm.tsx
index 09501add..fdf42e28 100644
--- a/src/components/accounting/condolence-expenses/CondolenceExpenseForm.tsx
+++ b/src/components/accounting/condolence-expenses/CondolenceExpenseForm.tsx
@@ -13,7 +13,7 @@ import { DatePicker } from '@/components/ui/date-picker';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
-import { NumberInput } from '@/components/ui/number-input';
+import { CurrencyInput } from '@/components/ui/currency-input';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
@@ -253,11 +253,9 @@ export function CondolenceExpenseFormModal({
- handleChange('cash_amount', v ?? 0)}
- min={0}
- useComma
+ onChange={(v: number | undefined) => handleChange('cash_amount', v ?? 0)}
/>
@@ -286,11 +284,9 @@ export function CondolenceExpenseFormModal({
- handleChange('gift_amount', v ?? 0)}
- min={0}
- useComma
+ onChange={(v: number | undefined) => handleChange('gift_amount', v ?? 0)}
/>
diff --git a/src/components/accounting/condolence-expenses/CondolenceExpenseList.tsx b/src/components/accounting/condolence-expenses/CondolenceExpenseList.tsx
index 790f24f9..2508bd6e 100644
--- a/src/components/accounting/condolence-expenses/CondolenceExpenseList.tsx
+++ b/src/components/accounting/condolence-expenses/CondolenceExpenseList.tsx
@@ -3,37 +3,34 @@
/**
* 경조사비 관리 - 목록 페이지
*
+ * UniversalListPage + clientSideFiltering 패턴 (품목관리와 동일)
* - 통계카드 (총건수/총금액/부조금합계/선물합계)
- * - 필터: 연도 Select, 구분 Select, 검색 Input
- * - 테이블 13컬럼 + 하단 합계행
- * - 등록/수정 모달 (Dialog)
+ * - 필터: 연도 Select, 구분 filterConfig, 검색 (클라이언트 사이드)
+ * - 테이블 + 등록/수정 모달
*/
import { useState, useEffect, useCallback, useMemo } from 'react';
+import { useRouter } from 'next/navigation';
import {
Heart,
DollarSign,
Banknote,
Gift,
- Plus,
- Edit,
Trash2,
Loader2,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { BadgeSm } from '@/components/atoms/BadgeSm';
import {
- IntegratedListTemplateV2,
+ UniversalListPage,
+ type UniversalListConfig,
type TableColumn,
type FilterFieldConfig,
- type FilterValues,
-} from '@/components/templates/IntegratedListTemplateV2';
-import { useColumnSettings } from '@/hooks/useColumnSettings';
-import { ColumnSettingsPopover } from '@/components/molecules/ColumnSettingsPopover';
+} from '@/components/templates/UniversalListPage';
import { toast } from 'sonner';
import { TableRow, TableCell } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
-import { MobileCard, InfoField } from '@/components/organisms/MobileCard';
+import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { formatAmount } from '@/lib/utils/amount';
@@ -48,7 +45,6 @@ import {
CATEGORY_OPTIONS,
type CondolenceExpense,
type CondolenceExpenseSummary,
- type CondolenceCategory,
} from './types';
// 연도 옵션 (당해 ~ 5년 전)
@@ -60,12 +56,14 @@ function getYearOptions() {
}));
}
-const TABLE_COLUMNS: TableColumn[] = [
+const BASE_PATH = '/accounting/condolence-expenses';
+
+const tableColumns: TableColumn[] = [
{ key: 'no', label: '번호', className: 'text-center w-[50px]' },
- { key: 'event_date', label: '경조사일자', className: 'px-2 w-[100px]' },
- { key: 'expense_date', label: '지출일자', className: 'px-2 w-[100px]' },
- { key: 'partner_name', label: '거래처명', className: 'px-2' },
- { key: 'description', label: '내역', className: 'px-2' },
+ { key: 'event_date', label: '경조사일자', className: 'px-2 w-[100px]', sortable: true, copyable: true },
+ { key: 'expense_date', label: '지출일자', className: 'px-2 w-[100px]', copyable: true },
+ { key: 'partner_name', label: '거래처명', className: 'px-2', sortable: true, copyable: true },
+ { key: 'description', label: '내역', className: 'px-2', copyable: true },
{ key: 'category', label: '구분', className: 'px-2 text-center w-[70px]' },
{ key: 'has_cash', label: '부조금', className: 'px-2 text-center w-[60px]' },
{ key: 'cash_method', label: '지출방법', className: 'px-2 w-[80px]' },
@@ -75,33 +73,28 @@ const TABLE_COLUMNS: TableColumn[] = [
{ key: 'gift_amount', label: '선물금액', className: 'px-2 text-right w-[100px]' },
{ key: 'total_amount', label: '총금액', className: 'px-2 text-right w-[100px]' },
{ key: 'memo', label: '비고', className: 'px-2' },
- { key: 'actions', label: '작업', className: 'text-center w-[60px]' },
];
export function CondolenceExpenseList() {
- // 필터 상태
+ const router = useRouter();
+
+ // 연도 필터
const [year, setYear] = useState(String(new Date().getFullYear()));
- const [searchTerm, setSearchTerm] = useState('');
- const [selectedItems, setSelectedItems] = useState>(new Set());
- const [currentPage, setCurrentPage] = useState(1);
- const itemsPerPage = 50;
// 필터 (filterConfig)
- const [filterCategory, setFilterCategory] = useState('all');
+ const [filterValues, setFilterValues] = useState>({
+ category: 'all',
+ });
- const filterConfig: FilterFieldConfig[] = useMemo(() => [
+ const filterConfig: FilterFieldConfig[] = [
{
key: 'category',
label: '구분',
- type: 'single' as const,
+ type: 'single',
options: CATEGORY_OPTIONS.map((o) => ({ value: o.value, label: o.label })),
allOptionLabel: '전체 구분',
},
- ], []);
-
- const filterValues: FilterValues = useMemo(() => ({
- category: filterCategory,
- }), [filterCategory]);
+ ];
// 모달 상태
const [isFormOpen, setIsFormOpen] = useState(false);
@@ -116,44 +109,29 @@ export function CondolenceExpenseList() {
const [data, setData] = useState([]);
const [summaryData, setSummaryData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
- const [totalCount, setTotalCount] = useState(0);
- // 컬럼 설정
- const {
- visibleColumns,
- allColumnsWithVisibility,
- columnWidths,
- setColumnWidth,
- toggleColumnVisibility,
- resetSettings,
- hasHiddenColumns,
- } = useColumnSettings({
- pageId: 'condolence-expenses',
- columns: TABLE_COLUMNS,
- alwaysVisibleKeys: ['no', 'partner_name', 'total_amount', 'actions'],
- });
+ // 선택
+ const [selectedItems, setSelectedItems] = useState>(new Set());
// 데이터 로드
const loadData = useCallback(async () => {
try {
setIsLoading(true);
+ const categoryFilter = filterValues.category as string;
const [listResult, summaryResult] = await Promise.all([
getCondolenceExpenses({
year: year ? Number(year) : undefined,
- category: filterCategory !== 'all' ? filterCategory : undefined,
- search: searchTerm || undefined,
- per_page: itemsPerPage,
- page: currentPage,
+ category: categoryFilter !== 'all' ? categoryFilter : undefined,
+ per_page: 200,
}),
getCondolenceExpenseSummary({
year: year ? Number(year) : undefined,
- category: filterCategory !== 'all' ? filterCategory : undefined,
+ category: categoryFilter !== 'all' ? categoryFilter : undefined,
}),
]);
if (listResult.success) {
setData(listResult.data);
- setTotalCount(listResult.pagination?.total ?? 0);
} else {
toast.error(listResult.error || '목록 조회 실패');
}
@@ -166,7 +144,7 @@ export function CondolenceExpenseList() {
} finally {
setIsLoading(false);
}
- }, [year, filterCategory, searchTerm, currentPage]);
+ }, [year, filterValues]);
useEffect(() => { loadData(); }, [loadData]);
@@ -181,13 +159,6 @@ export function CondolenceExpenseList() {
];
}, [summaryData]);
- // 하단 합계
- const totals = useMemo(() => ({
- cash: data.reduce((sum, d) => sum + (d.cash_amount || 0), 0),
- gift: data.reduce((sum, d) => sum + (d.gift_amount || 0), 0),
- total: data.reduce((sum, d) => sum + (d.total_amount || 0), 0),
- }), [data]);
-
// 핸들러
const handleCreate = () => { setEditingItem(null); setIsFormOpen(true); };
const handleEdit = (item: CondolenceExpense) => { setEditingItem(item); setIsFormOpen(true); };
@@ -219,34 +190,39 @@ export function CondolenceExpenseList() {
loadData();
};
- // 선택
- const toggleSelection = useCallback((id: string) => {
- setSelectedItems(prev => {
- const next = new Set(prev);
- next.has(id) ? next.delete(id) : next.add(id);
- return next;
- });
- }, []);
+ // 선택 핸들러
+ const toggleSelection = (id: string) => {
+ const next = new Set(selectedItems);
+ next.has(id) ? next.delete(id) : next.add(id);
+ setSelectedItems(next);
+ };
- const toggleSelectAll = useCallback(() => {
- setSelectedItems(prev =>
- prev.size === data.length ? new Set() : new Set(data.map(d => String(d.id)))
- );
- }, [data]);
+ const toggleSelectAll = () => {
+ if (selectedItems.size === data.length && data.length > 0) {
+ setSelectedItems(new Set());
+ } else {
+ setSelectedItems(new Set(data.map((d) => String(d.id))));
+ }
+ };
// 테이블 행
- const renderTableRow = useCallback((
+ const renderTableRow = (
item: CondolenceExpense,
_index: number,
globalIndex: number,
+ handlers: { isSelected: boolean; onToggle: () => void }
) => {
+ const { isSelected, onToggle } = handlers;
const badge = CATEGORY_BADGE[item.category];
return (
handleEdit(item)}
>
+ e.stopPropagation()} className="text-center">
+
+
{globalIndex}
{item.event_date || '-'}
{item.expense_date || '-'}
@@ -263,59 +239,138 @@ export function CondolenceExpenseList() {
{item.has_gift ? formatAmount(item.gift_amount) : '-'}
{formatAmount(item.total_amount)}
{item.memo || '-'}
- e.stopPropagation()}>
-
-
);
- }, []);
+ };
// 모바일 카드
- const renderMobileCard = useCallback((
+ const renderMobileCard = (
item: CondolenceExpense,
_index: number,
globalIndex: number,
+ handlers: { isSelected: boolean; onToggle: () => void }
) => {
+ const { isSelected, onToggle } = handlers;
const badge = CATEGORY_BADGE[item.category];
return (
- handleEdit(item)}
headerBadges={
{badge.label}
}
+ title={item.partner_name}
+ statusBadge={
+ {formatAmount(item.total_amount)}
+ }
infoGrid={
-
+
}
- onClick={() => handleEdit(item)}
/>
);
- }, []);
+ };
- // 합계 표시 (tableHeaderActions에 인라인)
- const summaryText = useMemo(() => {
- if (data.length === 0) return null;
- return (
-
- 부조금 {formatAmount(totals.cash)}
- 선물 {formatAmount(totals.gift)}
- 합계 {formatAmount(totals.total)}
-
- );
- }, [data.length, totals]);
+ // UniversalListPage config
+ const config: UniversalListConfig = {
+ title: '경조사비 관리',
+ description: '거래처/임직원 경조사비 관리',
+ icon: Heart,
+ basePath: BASE_PATH,
+ idField: 'id',
+
+ actions: {
+ getList: async () => ({
+ success: true,
+ data,
+ totalCount: data.length,
+ }),
+ deleteItem: async (id) => {
+ const result = await deleteCondolenceExpense(id);
+ if (result.success) loadData();
+ return result;
+ },
+ },
+
+ columns: tableColumns,
+
+ computeStats: () => stats,
+
+ searchPlaceholder: '거래처명, 내역, 비고 검색...',
+
+ // 연도 Select
+ dateRangeSelector: {
+ enabled: true,
+ hideDateInputs: true,
+ showPresets: false,
+ extraActions: (
+
+ ),
+ },
+
+ itemsPerPage: 50,
+
+ // 클라이언트 사이드 필터링 (검색 깜빡임 없음)
+ clientSideFiltering: true,
+
+ searchFilter: (item, searchValue) => {
+ const q = searchValue.toLowerCase();
+ return (
+ (item.partner_name || '').toLowerCase().includes(q) ||
+ (item.description || '').toLowerCase().includes(q) ||
+ (item.memo || '').toLowerCase().includes(q)
+ );
+ },
+
+ // 구분 필터
+ filterConfig,
+ initialFilters: filterValues,
+ filterTitle: '경조사비 필터',
+
+ customFilterFn: (items, fv) => {
+ const cat = fv.category as string;
+ if (!cat || cat === 'all') return items;
+ return items.filter((item) => item.category === cat);
+ },
+
+ // 등록 버튼
+ headerActions: () => (
+
+ ),
+
+ renderTableRow,
+ renderMobileCard,
+
+ renderDialogs: () => (
+
+ ),
+ };
if (isLoading) {
return (
@@ -330,88 +385,20 @@ export function CondolenceExpenseList() {
return (
<>
-
- // 헤더
- title="경조사비 관리"
- description="거래처/임직원 경조사비 관리"
- icon={Heart}
-
- // 연도 선택 (dateRangeSelector 대신 extraActions 사용)
- dateRangeSelector={{
- enabled: true,
- hideDateInputs: true,
- showPresets: false,
- extraActions: (
-
- ),
+
+ config={config}
+ initialData={data}
+ initialTotalCount={data.length}
+ externalSelection={{
+ selectedItems,
+ onToggleSelection: toggleSelection,
+ onToggleSelectAll: toggleSelectAll,
+ setSelectedItems,
+ getItemId: (item) => String(item.id),
}}
-
- // 검색
- searchValue={searchTerm}
- onSearchChange={(q) => { setSearchTerm(q); setCurrentPage(1); }}
- searchPlaceholder="거래처명, 내역, 비고 검색..."
-
- // 등록 버튼
- createButton={{ label: '등록', onClick: handleCreate }}
-
- // 통계
- stats={stats}
-
- // 필터
- filterConfig={filterConfig}
- filterValues={filterValues}
- onFilterChange={(key, value) => {
- if (key === 'category') { setFilterCategory(value as string); setCurrentPage(1); }
+ onFilterChange={(newFilters) => {
+ setFilterValues(newFilters);
}}
- onFilterReset={() => { setFilterCategory('all'); setCurrentPage(1); }}
- filterTitle="경조사비 필터"
-
- // 테이블 + 컬럼 설정
- tableColumns={visibleColumns}
- columnSettings={{
- columnWidths,
- onColumnResize: setColumnWidth,
- settingsPopover: (
-
- ),
- }}
-
- // 데이터
- data={data}
- selectedItems={selectedItems}
- onToggleSelection={toggleSelection}
- onToggleSelectAll={toggleSelectAll}
- getItemId={(item) => String(item.id)}
-
- // 렌더링
- renderTableRow={renderTableRow}
- renderMobileCard={renderMobileCard}
- tableHeaderActions={summaryText}
-
- // 페이지네이션
- pagination={{
- currentPage,
- totalPages: Math.ceil(totalCount / itemsPerPage),
- totalItems: totalCount,
- itemsPerPage,
- onPageChange: setCurrentPage,
- }}
-
- isLoading={isLoading}
/>
{/* 등록/수정 모달 */}
@@ -421,16 +408,6 @@ export function CondolenceExpenseList() {
editItem={editingItem}
onSuccess={handleFormSuccess}
/>
-
- {/* 삭제 확인 */}
-
>
);
}