feat(WEB): 리스트 페이지 UI 레이아웃 표준화
- 공통 레이아웃 패턴 적용: [달력] → [프리셋] → [검색창] → [버튼들] - beforeTableContent → headerActions + createButton 마이그레이션 - DateRangeSelector extraActions prop 활용하여 검색창 통합 - PricingListClient 테이블 행 클릭 → 상세 이동 기능 추가 - 회계 관련 페이지 (입금/출금/매입/매출/어음/카드/예상지출 등) 정리 - 건설 관련 페이지 검색 영역 정리 - 부모 메뉴 리다이렉트 컴포넌트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -242,6 +242,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
|
||||
// 커스텀 필터 함수
|
||||
customFilterFn: (items) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
let result = [...items];
|
||||
|
||||
// 거래처 필터
|
||||
|
||||
@@ -48,7 +48,8 @@ interface PaginationMeta {
|
||||
// ===== API → Frontend 변환 =====
|
||||
function transformItem(item: BankTransactionApiItem): BankTransaction {
|
||||
return {
|
||||
id: String(item.id),
|
||||
// 입금/출금 테이블이 별도이므로 type을 접두어로 붙여 고유 ID 생성
|
||||
id: `${item.type}-${item.id}`,
|
||||
bankName: item.bank_name,
|
||||
accountName: item.account_name,
|
||||
transactionDate: item.transaction_date,
|
||||
|
||||
@@ -279,18 +279,16 @@ export function BankTransactionInquiry({
|
||||
onEndDateChange: setEndDate,
|
||||
},
|
||||
|
||||
// 테이블 상단 콘텐츠 (새로고침 버튼)
|
||||
beforeTableContent: (
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<Button variant="outline" onClick={handleRefresh} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
새로고침
|
||||
</Button>
|
||||
</div>
|
||||
// 헤더 액션: 새로고침 버튼
|
||||
headerActions: () => (
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
{isLoading ? '조회중...' : '새로고침'}
|
||||
</Button>
|
||||
),
|
||||
|
||||
// 테이블 헤더 액션 (3개 필터)
|
||||
|
||||
@@ -429,6 +429,42 @@ export function BillManagementClient({
|
||||
icon: Plus,
|
||||
},
|
||||
|
||||
// 헤더 액션: 상태 선택 + 저장 + 수취/발행 라디오
|
||||
headerActions: () => (
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroup
|
||||
value={billTypeFilter}
|
||||
onValueChange={(value) => { setBillTypeFilter(value); loadData(1); }}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<RadioGroupItem value="received" id="received" />
|
||||
<Label htmlFor="received" className="cursor-pointer text-sm">수취</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<RadioGroupItem value="issued" id="issued" />
|
||||
<Label htmlFor="issued" className="cursor-pointer text-sm">발행</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
<SelectValue placeholder="상태" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BILL_STATUS_FILTER_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={handleSave} size="sm" disabled={isLoading}>
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 테이블 헤더 액션 (필터)
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -473,44 +509,6 @@ export function BillManagementClient({
|
||||
</div>
|
||||
),
|
||||
|
||||
// beforeTableContent: 상태 선택 + 저장 + 수취/발행 라디오
|
||||
beforeTableContent: (
|
||||
<div className="flex items-center gap-4">
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectValue placeholder="보관중" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{BILL_STATUS_FILTER_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button onClick={handleSave} className="bg-blue-500 hover:bg-blue-600" disabled={isLoading}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
저장
|
||||
</Button>
|
||||
|
||||
<RadioGroup
|
||||
value={billTypeFilter}
|
||||
onValueChange={(value) => { setBillTypeFilter(value); loadData(1); }}
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="received" id="received" />
|
||||
<Label htmlFor="received" className="cursor-pointer">수취</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="issued" id="issued" />
|
||||
<Label htmlFor="issued" className="cursor-pointer">발행</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 렌더링 함수
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfMonth, endOfMonth } from 'date-fns';
|
||||
import { CreditCard, Plus, RefreshCw, Save, Loader2 } from 'lucide-react';
|
||||
import { CreditCard, Plus, RefreshCw, Save, Loader2, Search } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -380,18 +380,66 @@ export function CardTransactionInquiry({
|
||||
},
|
||||
filterTitle: '카드 필터',
|
||||
|
||||
// 헤더 액션 (등록 버튼)
|
||||
// 헤더 액션: 계정과목명 Select + 저장 + 새로고침
|
||||
headerActions: () => (
|
||||
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/card-transactions?mode=new')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
카드내역 등록
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={handleSaveAccountSubject} size="sm">
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
저장
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
)}
|
||||
{isLoading ? '조회중...' : '새로고침'}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 등록 버튼
|
||||
createButton: {
|
||||
label: '카드내역 등록',
|
||||
icon: Plus,
|
||||
onClick: () => router.push('/ko/accounting/card-transactions?mode=new'),
|
||||
},
|
||||
|
||||
// 커스텀 필터 함수
|
||||
customFilterFn: (items) => {
|
||||
if (cardFilter === 'all') return items;
|
||||
return items.filter((item) => item.cardName === cardFilter);
|
||||
if (!items || items.length === 0) return items;
|
||||
let result = [...items];
|
||||
|
||||
// 검색어 필터
|
||||
if (searchQuery) {
|
||||
const search = searchQuery.toLowerCase();
|
||||
result = result.filter((item) =>
|
||||
item.card.toLowerCase().includes(search) ||
|
||||
item.cardName.toLowerCase().includes(search) ||
|
||||
item.user.toLowerCase().includes(search) ||
|
||||
item.merchantName.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
// 카드명 필터
|
||||
if (cardFilter !== 'all') {
|
||||
result = result.filter((item) => item.cardName === cardFilter);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
// 커스텀 정렬 함수
|
||||
@@ -417,8 +465,15 @@ export function CardTransactionInquiry({
|
||||
},
|
||||
|
||||
// 날짜 선택기 (헤더 액션)
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
searchValue: searchQuery,
|
||||
onSearchChange: setSearchQuery,
|
||||
searchPlaceholder: '카드, 카드명, 사용자, 가맹점명 검색...',
|
||||
|
||||
dateRangeSelector: {
|
||||
enabled: true,
|
||||
showPresets: true,
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange: setStartDate,
|
||||
@@ -428,42 +483,6 @@ export function CardTransactionInquiry({
|
||||
// 선택 항목 변경 콜백
|
||||
onSelectionChange: setSelectedItems,
|
||||
|
||||
// 테이블 상단 콘텐츠 (계정과목명 + 저장 + 새로고침)
|
||||
beforeTableContent: (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="계정과목명 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
onClick={handleSaveAccountSubject}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" onClick={handleRefresh} disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
새로고침
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 테이블 헤더 액션 (2개 필터)
|
||||
tableHeaderActions: () => (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
Save,
|
||||
Trash2,
|
||||
RefreshCw,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
@@ -103,6 +105,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
const [endDate, setEndDate] = useState('2025-09-03');
|
||||
const [depositData, setDepositData] = useState<DepositRecord[]>(initialData);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 인라인 필터 상태 (tableHeaderActions에서 사용)
|
||||
const [vendorFilter, setVendorFilter] = useState<string>('all');
|
||||
@@ -262,7 +265,19 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
|
||||
// 커스텀 필터 함수 (인라인 필터 사용)
|
||||
customFilterFn: (items, filterValues) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
return items.filter((item) => {
|
||||
// 검색어 필터
|
||||
if (searchQuery) {
|
||||
const search = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
item.depositorName.toLowerCase().includes(search) ||
|
||||
item.accountName.toLowerCase().includes(search) ||
|
||||
(item.note?.toLowerCase().includes(search) || false) ||
|
||||
(item.vendorName?.toLowerCase().includes(search) || false);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// 거래처 필터
|
||||
if (vendorFilter !== 'all' && item.vendorName !== vendorFilter) {
|
||||
return false;
|
||||
@@ -295,9 +310,16 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
return sorted;
|
||||
},
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
searchValue: searchQuery,
|
||||
onSearchChange: setSearchQuery,
|
||||
searchPlaceholder: '입금자명, 계좌명, 적요, 거래처 검색...',
|
||||
|
||||
// 공통 헤더 옵션
|
||||
dateRangeSelector: {
|
||||
enabled: true,
|
||||
showPresets: true,
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange: setStartDate,
|
||||
@@ -325,14 +347,45 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
},
|
||||
filterTitle: '입금 필터',
|
||||
|
||||
// 헤더 액션 (등록 버튼)
|
||||
headerActions: () => (
|
||||
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/deposits?mode=new')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
입금등록
|
||||
</Button>
|
||||
// 헤더 액션: 계정과목명 Select + 저장 + 새로고침
|
||||
headerActions: ({ selectedItems }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => handleSaveAccountSubject(selectedItems)} size="sm">
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
저장
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-1 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
{isRefreshing ? '조회중...' : '새로고침'}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 등록 버튼
|
||||
createButton: {
|
||||
label: '입금등록',
|
||||
icon: Plus,
|
||||
onClick: () => router.push('/ko/accounting/deposits?mode=new'),
|
||||
},
|
||||
|
||||
// Stats 카드
|
||||
computeStats: (): StatCard[] => [
|
||||
{ label: '총 입금', value: `${stats.totalDeposit.toLocaleString()}원`, icon: Banknote, iconColor: 'text-blue-500' },
|
||||
@@ -341,44 +394,9 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
{ label: '입금유형 미설정', value: `${stats.depositTypeUnsetCount}건`, icon: Banknote, iconColor: 'text-red-500' },
|
||||
],
|
||||
|
||||
// beforeTableContent: 계정과목명 Select + 저장 버튼 + 새로고침
|
||||
beforeTableContent: (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="계정과목명 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
{isRefreshing ? '조회중...' : '새로고침'}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
// tableHeaderActions: 3개 인라인 필터
|
||||
tableHeaderActions: ({ selectedItems }) => (
|
||||
tableHeaderActions: (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 저장 버튼 */}
|
||||
<Button onClick={() => handleSaveAccountSubject(selectedItems)} className="bg-blue-500 hover:bg-blue-600">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
저장
|
||||
</Button>
|
||||
|
||||
{/* 거래처 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
@@ -556,6 +574,7 @@ export function DepositManagement({ initialData, initialPagination }: DepositMan
|
||||
vendorOptions,
|
||||
tableTotals,
|
||||
isRefreshing,
|
||||
searchQuery,
|
||||
handleRowClick,
|
||||
handleEdit,
|
||||
handleRefresh,
|
||||
|
||||
@@ -902,51 +902,9 @@ export function ExpectedExpenseManagement({
|
||||
},
|
||||
filterTitle: '예상비용 필터',
|
||||
|
||||
// 테이블 헤더 액션 (거래처/정렬 필터)
|
||||
tableHeaderActions: () => (
|
||||
// 헤더 액션: 선택 기반 액션 버튼들
|
||||
headerActions: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 거래처 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[140px] h-8 text-sm">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vendorFilterOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 정렬 필터 (최신순/등록순) */}
|
||||
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
<SelectTrigger className="w-[100px] h-8 text-sm">
|
||||
<SelectValue placeholder="최신순" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SORT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 테이블 앞 컨텐츠 (액션 버튼)
|
||||
beforeTableContent: (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* 등록 버튼 */}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleOpenCreateDialog}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
등록
|
||||
</Button>
|
||||
|
||||
{/* 예상 지급일 변경 버튼 - 1개 이상 선택 시 활성화 */}
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -983,6 +941,46 @@ export function ExpectedExpenseManagement({
|
||||
</div>
|
||||
),
|
||||
|
||||
// 등록 버튼
|
||||
createButton: {
|
||||
label: '등록',
|
||||
icon: Plus,
|
||||
onClick: handleOpenCreateDialog,
|
||||
},
|
||||
|
||||
// 테이블 헤더 액션 (거래처/정렬 필터)
|
||||
tableHeaderActions: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 거래처 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[140px] h-8 text-sm">
|
||||
<SelectValue placeholder="전체" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vendorFilterOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 정렬 필터 (최신순/등록순) */}
|
||||
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
|
||||
<SelectTrigger className="w-[100px] h-8 text-sm">
|
||||
<SelectValue placeholder="최신순" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SORT_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
|
||||
// Stats 카드
|
||||
computeStats: (): StatCard[] => {
|
||||
const totalExpense = filteredRawData.reduce((sum, d) => sum + d.amount, 0);
|
||||
@@ -1011,7 +1009,7 @@ export function ExpectedExpenseManagement({
|
||||
vendorFilter,
|
||||
vendorFilterOptions,
|
||||
sortOption,
|
||||
selectedItems,
|
||||
selectedItems.size,
|
||||
handleOpenCreateDialog,
|
||||
handleOpenDateChangeDialog,
|
||||
handleElectronicApproval,
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
Pencil,
|
||||
Save,
|
||||
Trash2,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -85,6 +87,7 @@ export function PurchaseManagement() {
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const [purchaseData, setPurchaseData] = useState<PurchaseRecord[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 통합 필터 상태 (filterConfig 기반)
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string | string[]>>({
|
||||
@@ -295,7 +298,17 @@ export function PurchaseManagement() {
|
||||
|
||||
// 커스텀 필터 함수 (filterValues 파라미터 사용)
|
||||
customFilterFn: (items, fv) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
return items.filter((item) => {
|
||||
// 검색어 필터
|
||||
if (searchQuery) {
|
||||
const search = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
item.purchaseNo.toLowerCase().includes(search) ||
|
||||
item.vendorName.toLowerCase().includes(search);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
const vendorVal = fv.vendor as string;
|
||||
const purchaseTypeVal = fv.purchaseType as string;
|
||||
const issuanceVal = fv.issuance as string;
|
||||
@@ -336,15 +349,45 @@ export function PurchaseManagement() {
|
||||
return sorted;
|
||||
},
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
searchValue: searchQuery,
|
||||
onSearchChange: setSearchQuery,
|
||||
searchPlaceholder: '매입번호, 거래처명 검색...',
|
||||
|
||||
// 공통 헤더 옵션
|
||||
dateRangeSelector: {
|
||||
enabled: true,
|
||||
showPresets: true,
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange: setStartDate,
|
||||
onEndDateChange: setEndDate,
|
||||
},
|
||||
|
||||
// 헤더 액션: 계정과목명 Select + 저장 버튼
|
||||
headerActions: ({ selectedItems }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_SELECTOR_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => handleSaveAccountSubject(selectedItems)} size="sm">
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 통합 필터 시스템 (PC: 인라인, 모바일: 바텀시트 자동 분기)
|
||||
filterConfig,
|
||||
initialFilters: filterValues,
|
||||
@@ -358,29 +401,6 @@ export function PurchaseManagement() {
|
||||
{ label: '세금계산서 수취 미확인', value: `${stats.taxInvoicePendingCount}건`, icon: Receipt, iconColor: 'text-red-500' },
|
||||
],
|
||||
|
||||
// beforeTableContent: 계정과목명 Select + 저장 버튼 (테이블 밖에 위치)
|
||||
beforeTableContent: ({ selectedItems }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="계정과목명 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_SELECTOR_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => handleSaveAccountSubject(selectedItems)} className="bg-blue-500 hover:bg-blue-600">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 테이블 하단 합계 행
|
||||
tableFooter: (
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
@@ -532,6 +552,7 @@ export function PurchaseManagement() {
|
||||
filterValues,
|
||||
selectedAccountSubject,
|
||||
tableTotals,
|
||||
searchQuery,
|
||||
handleRowClick,
|
||||
handleEdit,
|
||||
handleTaxInvoiceToggle,
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
Pencil,
|
||||
Save,
|
||||
Trash2,
|
||||
Search,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
@@ -97,6 +99,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
const [startDate, setStartDate] = useState('2025-01-01');
|
||||
const [endDate, setEndDate] = useState('2025-12-31');
|
||||
const [salesData, setSalesData] = useState<SalesRecord[]>(initialData);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 통합 필터 상태 (filterConfig 사용)
|
||||
const [filterValues, setFilterValues] = useState<Record<string, string | string[]>>({
|
||||
@@ -297,7 +300,18 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
|
||||
// 커스텀 필터 함수 (filterConfig 사용)
|
||||
customFilterFn: (items, fv) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
return items.filter((item) => {
|
||||
// 검색어 필터
|
||||
if (searchQuery) {
|
||||
const search = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
item.salesNo.toLowerCase().includes(search) ||
|
||||
item.vendorName.toLowerCase().includes(search) ||
|
||||
item.note.toLowerCase().includes(search);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
const vendorVal = fv.vendor as string;
|
||||
const salesTypeVal = fv.salesType as string;
|
||||
const issuanceVal = fv.issuance as string;
|
||||
@@ -342,14 +356,43 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
return sorted;
|
||||
},
|
||||
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
searchValue: searchQuery,
|
||||
onSearchChange: setSearchQuery,
|
||||
searchPlaceholder: '매출번호, 거래처명, 비고 검색...',
|
||||
|
||||
// 공통 헤더 옵션
|
||||
dateRangeSelector: {
|
||||
enabled: true,
|
||||
showPresets: true,
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange: setStartDate,
|
||||
onEndDateChange: setEndDate,
|
||||
},
|
||||
// 헤더 액션 (계정과목명 Select + 저장 버튼)
|
||||
headerActions: ({ selectedItems }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_SELECTOR_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => handleSaveAccountSubject(selectedItems)} size="sm">
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
createButton: {
|
||||
label: '매출 등록',
|
||||
onClick: handleCreate,
|
||||
@@ -368,29 +411,6 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
initialFilters: filterValues,
|
||||
filterTitle: '매출 필터',
|
||||
|
||||
// beforeTableContent: 계정과목명 Select + 저장 버튼 (테이블 밖에 위치)
|
||||
beforeTableContent: ({ selectedItems }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="계정과목명 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_SELECTOR_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => handleSaveAccountSubject(selectedItems)} className="bg-blue-500 hover:bg-blue-600">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
|
||||
// 테이블 하단 합계 행
|
||||
tableFooter: (
|
||||
<TableRow className="bg-muted/50 font-medium">
|
||||
@@ -534,6 +554,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
||||
filterValues,
|
||||
selectedAccountSubject,
|
||||
tableTotals,
|
||||
searchQuery,
|
||||
handleRowClick,
|
||||
handleEdit,
|
||||
handleCreate,
|
||||
|
||||
@@ -211,6 +211,7 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement
|
||||
|
||||
// 커스텀 필터 함수
|
||||
customFilterFn: (items, filterValues) => {
|
||||
if (!items || items.length === 0) return items;
|
||||
return items.filter((item) => {
|
||||
// 구분 필터
|
||||
const categoryFilter = filterValues.category as string;
|
||||
|
||||
@@ -73,14 +73,13 @@ import { toast } from 'sonner';
|
||||
|
||||
// ===== 테이블 컬럼 정의 =====
|
||||
const tableColumns = [
|
||||
{ key: 'withdrawalDate', label: '출금일' },
|
||||
{ key: 'accountName', label: '출금계좌' },
|
||||
{ key: 'recipientName', label: '수취인명' },
|
||||
{ key: 'withdrawalAmount', label: '출금금액', className: 'text-right' },
|
||||
{ key: 'vendorName', label: '거래처' },
|
||||
{ key: 'note', label: '적요' },
|
||||
{ key: 'withdrawalType', label: '출금유형', className: 'text-center' },
|
||||
{ key: 'actions', label: '작업', className: 'text-center w-[80px]' },
|
||||
{ key: 'withdrawalDate', label: '출금일', className: 'w-[100px]' },
|
||||
{ key: 'accountName', label: '출금계좌', className: 'min-w-[120px]' },
|
||||
{ key: 'recipientName', label: '수취인명', className: 'min-w-[100px]' },
|
||||
{ key: 'withdrawalAmount', label: '출금금액', className: 'text-right w-[110px]' },
|
||||
{ key: 'vendorName', label: '거래처', className: 'min-w-[100px]' },
|
||||
{ key: 'note', label: '적요', className: 'min-w-[150px]' },
|
||||
{ key: 'withdrawalType', label: '출금유형', className: 'text-center w-[90px]' },
|
||||
];
|
||||
|
||||
// ===== 컴포넌트 Props =====
|
||||
@@ -112,6 +111,9 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
// 상단 계정과목명 선택 (저장용)
|
||||
const [selectedAccountSubject, setSelectedAccountSubject] = useState<string>('unset');
|
||||
|
||||
// 검색어 상태 (헤더에서 직접 관리)
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 로딩 상태
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
@@ -297,17 +299,23 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
},
|
||||
filterTitle: '출금 필터',
|
||||
|
||||
// 헤더 액션 (등록 버튼)
|
||||
headerActions: () => (
|
||||
<Button className="ml-auto" onClick={() => router.push('/ko/accounting/withdrawals?mode=new')}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
출금등록
|
||||
</Button>
|
||||
),
|
||||
// 검색창 숨김 (dateRangeSelector extraActions로 렌더링)
|
||||
hideSearch: true,
|
||||
|
||||
// 커스텀 필터 함수
|
||||
// 커스텀 필터 함수 (검색 + 필터)
|
||||
customFilterFn: (items) => {
|
||||
return items.filter((item) => {
|
||||
// 검색어 필터
|
||||
if (searchQuery) {
|
||||
const search = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
item.recipientName.toLowerCase().includes(search) ||
|
||||
item.accountName.toLowerCase().includes(search) ||
|
||||
item.note.toLowerCase().includes(search) ||
|
||||
item.vendorName.toLowerCase().includes(search);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// 거래처 필터
|
||||
if (vendorFilter !== 'all' && item.vendorName !== vendorFilter) {
|
||||
return false;
|
||||
@@ -342,23 +350,30 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
return sorted;
|
||||
},
|
||||
|
||||
// 날짜 범위 선택기
|
||||
// 검색창 (공통 컴포넌트에서 자동 생성)
|
||||
hideSearch: true,
|
||||
searchValue: searchQuery,
|
||||
onSearchChange: setSearchQuery,
|
||||
|
||||
// 날짜 범위 선택기 (달력 | 프리셋버튼 | 검색창(자동) - 한 줄)
|
||||
dateRangeSelector: {
|
||||
enabled: true,
|
||||
showPresets: true,
|
||||
startDate,
|
||||
endDate,
|
||||
onStartDateChange: setStartDate,
|
||||
onEndDateChange: setEndDate,
|
||||
},
|
||||
|
||||
// beforeTableContent: 계정과목명 + 저장 + 새로고침
|
||||
beforeTableContent: (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
// 헤더 액션: 계정과목명 Select + 저장 + 새로고침
|
||||
headerActions: ({ selectedItems }) => {
|
||||
const selectedArray = withdrawalData.filter(item => selectedItems.has(item.id));
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700">계정과목명</span>
|
||||
<span className="text-sm font-medium text-gray-700 whitespace-nowrap">계정과목명</span>
|
||||
<Select value={selectedAccountSubject} onValueChange={setSelectedAccountSubject}>
|
||||
<SelectTrigger className="w-[150px]">
|
||||
<SelectValue placeholder="계정과목명 선택" />
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ACCOUNT_SUBJECT_OPTIONS.map((option) => (
|
||||
@@ -368,29 +383,33 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => handleSaveAccountSubject(selectedArray)} size="sm">
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
저장
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-1 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
{isRefreshing ? '조회중...' : '새로고침'}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
{isRefreshing ? '조회중...' : '새로고침'}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
// tableHeaderActions: 저장 버튼 + 인라인 필터들
|
||||
tableHeaderActions: ({ selectedItems }) => (
|
||||
// 등록 버튼
|
||||
createButton: {
|
||||
label: '출금등록',
|
||||
icon: Plus,
|
||||
onClick: () => router.push('/ko/accounting/withdrawals?mode=new'),
|
||||
},
|
||||
|
||||
// tableHeaderActions: 필터만 (거래처, 출금유형, 정렬)
|
||||
tableHeaderActions: () => (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Button
|
||||
onClick={() => handleSaveAccountSubject(selectedItems)}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
저장
|
||||
</Button>
|
||||
|
||||
{/* 거래처 필터 */}
|
||||
<Select value={vendorFilter} onValueChange={setVendorFilter}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
@@ -446,7 +465,6 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -506,29 +524,6 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
{WITHDRAWAL_TYPE_LABELS[item.withdrawalType]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
{/* 작업 */}
|
||||
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
|
||||
{handlers.isSelected && (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-gray-600 hover:text-gray-700 hover:bg-gray-50"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
|
||||
onClick={() => handlers.onDelete?.(item)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
},
|
||||
@@ -576,9 +571,11 @@ export function WithdrawalManagement({ initialData, initialPagination }: Withdra
|
||||
}),
|
||||
[
|
||||
initialData,
|
||||
withdrawalData,
|
||||
stats,
|
||||
startDate,
|
||||
endDate,
|
||||
searchQuery,
|
||||
vendorFilter,
|
||||
withdrawalTypeFilter,
|
||||
sortOption,
|
||||
|
||||
Reference in New Issue
Block a user