Files
sam-react-prod/src/components/hr/CardManagement/index.tsx
유병철 012a661a19 refactor(WEB): 회계/결재/건설 등 공통화 3차 및 검색/상태 유틸 추가
- search.ts: 범용 검색 유틸리티 추출 (텍스트/날짜/상태 필터링)
- status-config.ts: 상태 설정 공통 유틸 추가
- 회계 모듈 types 간소화 및 컬럼 설정 공통 패턴 적용
- 회계 page.tsx 통일 (bad-debt/bills/deposits/sales 등 9개)
- 결재함(승인/기안/참조) 공통 패턴 적용
- 건설 모듈 견적/인수인계/이슈/기성 등 코드 정리
- IntegratedListTemplateV2 개선
- LanguageSelect/ThemeSelect 정리
- 체크리스트 문서 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:26:27 +09:00

301 lines
11 KiB
TypeScript

'use client';
/**
* 카드관리 - 카드 목록 페이지 (UniversalListPage 공통 구조)
*
* - 달력 + 프리셋 버튼 (이번달, 지난달, D-2월~D-5월)
* - 통계카드 4개 (전체/결제예정/총한도/잔여한도)
* - 수기 카드 등록 버튼
* - 카드사/상태 필터 (테이블 카드 내부)
* - 범례 (수기/연동 카드)
* - 체크박스 없음
*/
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { format, startOfMonth, endOfMonth } from 'date-fns';
import { CreditCard, Wallet, PiggyBank, TrendingDown } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { TableRow, TableCell } from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
UniversalListPage,
type UniversalListConfig,
type SelectionHandlers,
type RowClickHandlers,
type ListParams,
type StatCard,
} from '@/components/templates/UniversalListPage';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import type { Card as CardType } from './types';
import {
CARD_COMPANIES,
CARD_STATUS_OPTIONS,
CARD_STATUS_LABELS,
CARD_STATUS_COLORS,
getCardCompanyLabel,
} from './types';
import { formatAmountWon as formatCurrency } from '@/lib/utils/amount';
import { getCards, getCardStats } from './actions';
export function CardManagement() {
const router = useRouter();
const itemsPerPage = 20;
// ===== 날짜 범위 상태 =====
const today = new Date();
const [startDate, setStartDate] = useState(() => format(startOfMonth(today), 'yyyy-MM-dd'));
const [endDate, setEndDate] = useState(() => format(endOfMonth(today), 'yyyy-MM-dd'));
// ===== 필터 상태 =====
const [cardCompanyFilter, setCardCompanyFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all');
// ===== 통계 (별도 API) =====
const [stats, setStats] = useState<StatCard[]>([]);
useEffect(() => {
getCardStats({ startDate, endDate }).then(result => {
if (result.success && result.data) {
setStats([
{ label: '전체', value: `${result.data.totalCount}`, icon: CreditCard, iconColor: 'text-blue-500' },
{ label: '결제예정', value: formatCurrency(result.data.upcomingPayment), icon: Wallet, iconColor: 'text-orange-500' },
{ label: '총한도', value: formatCurrency(result.data.totalLimit), icon: PiggyBank, iconColor: 'text-green-500' },
{ label: '잔여한도', value: formatCurrency(result.data.remainingLimit), icon: TrendingDown, iconColor: 'text-purple-500' },
]);
}
});
}, [startDate, endDate]);
// ===== 핸들러 =====
const handleRowClick = useCallback((item: CardType) => {
router.push(`/ko/hr/card-management/${item.id}`);
}, [router]);
const handleCreate = useCallback(() => {
router.push('/ko/hr/card-management?mode=new');
}, [router]);
// ===== Config =====
const config: UniversalListConfig<CardType> = useMemo(
() => ({
title: '카드 관리',
description: '관련 기능 및 카드 목록을 관리합니다.',
icon: CreditCard,
basePath: '/hr/card-management',
idField: 'id',
showCheckbox: false,
// 날짜 범위 선택기 + 프리셋 버튼
dateRangeSelector: {
enabled: true,
showPresets: true,
presets: ['thisMonth', 'lastMonth', 'twoMonthsAgo', 'threeMonthsAgo', 'fourMonthsAgo', 'fiveMonthsAgo'],
presetLabels: {
thisMonth: '이번달',
lastMonth: '지난달',
},
startDate,
endDate,
onStartDateChange: setStartDate,
onEndDateChange: setEndDate,
},
// 수기 카드 등록 버튼
createButton: {
label: '수기 카드 등록',
onClick: handleCreate,
},
// 통계카드 (별도 API에서 로드)
stats,
// API 액션
actions: {
getList: async (params?: ListParams) => {
try {
const result = await getCards({
startDate,
endDate,
cardCompany: cardCompanyFilter,
status: statusFilter,
search: params?.search || '',
page: params?.page || 1,
per_page: itemsPerPage,
});
if (result.success && result.data) {
return {
success: true,
data: result.data,
totalCount: result.pagination?.total || result.data.length,
totalPages: result.pagination?.lastPage || 1,
};
}
return { success: false, error: result.error };
} catch {
return { success: false, error: '서버 오류가 발생했습니다.' };
}
},
},
// 테이블 컬럼
columns: [
{ key: 'no', label: 'No.', className: 'text-center w-[60px]' },
{ key: 'cardCompany', label: '카드사', className: 'min-w-[90px]' },
{ key: 'cardNumber', label: '카드번호', className: 'min-w-[160px]' },
{ key: 'cardName', label: '카드명', className: 'min-w-[150px]' },
{ key: 'department', label: '부서', className: 'min-w-[80px]' },
{ key: 'user', label: '사용자', className: 'min-w-[80px]' },
{ key: 'usage', label: '사용현황', className: 'min-w-[180px]' },
{ key: 'status', label: '상태', className: 'text-center min-w-[70px]' },
],
itemsPerPage,
searchPlaceholder: '카드명, 카드번호, 사용자명 검색...',
// 테이블 카드 내부 필터 (카드사, 상태)
tableHeaderActions: (
<div className="flex items-center gap-2">
<Select value={cardCompanyFilter} onValueChange={setCardCompanyFilter}>
<SelectTrigger className="min-w-[130px] w-auto h-9">
<SelectValue placeholder="카드사" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"> </SelectItem>
{CARD_COMPANIES.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="min-w-[100px] w-auto h-9">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{CARD_STATUS_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
),
// 테이블 행 렌더링
renderTableRow: (
item: CardType,
_index: number,
globalIndex: number,
_handlers: SelectionHandlers & RowClickHandlers<CardType>
) => {
const usagePercent = item.totalLimit > 0
? Math.min(Math.round((item.usedAmount / item.totalLimit) * 100), 100)
: 0;
return (
<TableRow
key={item.id}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => handleRowClick(item)}
>
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
<TableCell className="text-sm">{getCardCompanyLabel(item.cardCompany)}</TableCell>
<TableCell className="text-sm font-mono">{item.cardNumber}</TableCell>
<TableCell className="text-sm">
<div className="flex items-center gap-1.5">
{item.cardName}
{item.isManual ? (
<span className="text-[10px] text-muted-foreground">()</span>
) : (
<span className="text-[10px] text-blue-500">()</span>
)}
</div>
</TableCell>
<TableCell className="text-sm">{item.user?.departmentName || '-'}</TableCell>
<TableCell className="text-sm">{item.user?.employeeName || '-'}</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="font-medium text-red-600">{formatCurrency(item.usedAmount)}</span>
<span className="text-muted-foreground">{usagePercent}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all"
style={{ width: `${usagePercent}%` }}
/>
</div>
</div>
</TableCell>
<TableCell className="text-center">
<Badge className={CARD_STATUS_COLORS[item.status]}>
{CARD_STATUS_LABELS[item.status]}
</Badge>
</TableCell>
</TableRow>
);
},
// 모바일 카드
renderMobileCard: (
item: CardType,
_index: number,
_globalIndex: number,
_handlers: SelectionHandlers & RowClickHandlers<CardType>
) => (
<ListMobileCard
key={item.id}
id={item.id}
title={item.cardName}
headerBadges={
<span className="text-xs text-muted-foreground">
{getCardCompanyLabel(item.cardCompany)}
</span>
}
statusBadge={
<Badge className={CARD_STATUS_COLORS[item.status]}>
{CARD_STATUS_LABELS[item.status]}
</Badge>
}
isSelected={false}
onToggleSelection={() => {}}
onClick={() => handleRowClick(item)}
infoGrid={
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<InfoField label="카드번호" value={item.cardNumber} />
<InfoField label="사용자" value={item.user?.employeeName || '-'} />
</div>
}
/>
),
// 테이블 카드 내부 하단 - 범례
tableFooter: (
<TableRow>
<TableCell colSpan={8} className="border-0">
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1.5">
<span className="inline-block w-2 h-2 rounded-full bg-gray-400" />
</div>
<div className="flex items-center gap-1.5">
<span className="inline-block w-2 h-2 rounded-full bg-blue-500" />
</div>
</div>
</TableCell>
</TableRow>
),
}),
[handleCreate, handleRowClick, startDate, endDate, cardCompanyFilter, statusFilter, stats]
);
return <UniversalListPage config={config} />;
}