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} /> - - {/* 삭제 확인 */} - ); }