'use client'; import { useState, useEffect, useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { CreditCard, Save, Trash2, X, Edit, Loader2, ExternalLink } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { CardNumberInput } from '@/components/ui/card-number-input'; import { formatCardNumber } from '@/lib/formatters'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog'; import { useDeleteDialog } from '@/hooks/useDeleteDialog'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import { ContentSkeleton } from '@/components/ui/skeleton'; import { toast } from 'sonner'; import { formatAmountWon as formatCurrency } from '@/lib/utils/amount'; import type { FieldErrors } from '@/lib/api/execute-server-action'; import type { Card as CardType, CardFormData, CardStatus } from './types'; import { CARD_COMPANIES, CARD_TYPE_OPTIONS, PAYMENT_DAY_OPTIONS, CARD_STATUS_LABELS, CARD_STATUS_COLORS, getCardCompanyLabel, } from './types'; import { createCard, updateCard, deleteCard, getActiveEmployees, getApprovalFormUrl, } from './actions'; function formatExpiryDate(value: string): string { if (value && value.length === 4) { return `${value.slice(0, 2)}/${value.slice(2)}`; } return value || '-'; } function getPaymentDayLabel(value: string): string { const option = PAYMENT_DAY_OPTIONS.find(o => o.value === value); return option?.label || value || '-'; } function getCardTypeLabel(value: string): string { const option = CARD_TYPE_OPTIONS.find(o => o.value === value); return option?.label || value || '-'; } // 폼 필드(camelCase) → API 필드(snake_case) 매핑 const FORM_TO_API_FIELD: Record = { cardCompany: 'card_company', cardType: 'card_type', cardNumber: 'card_number', cardName: 'card_name', alias: 'alias', expiryDate: 'expiry_date', csv: 'csv', paymentDay: 'payment_day', pinPrefix: 'card_password', totalLimit: 'total_limit', usedAmount: 'used_amount', remainingLimit: 'remaining_limit', status: 'status', userId: 'assigned_user_id', memo: 'memo', }; // card prop → formData 변환 헬퍼 function buildFormData(card?: CardType): CardFormData { return { cardCompany: card?.cardCompany || '', cardType: card?.cardType || '', cardNumber: card?.cardNumber || '', cardName: card?.cardName || '', alias: card?.alias || '', expiryDate: card?.expiryDate || '', csv: card?.csv || '', paymentDay: card?.paymentDay || '', pinPrefix: '', totalLimit: card?.totalLimit || 0, usedAmount: card?.usedAmount || 0, remainingLimit: card?.remainingLimit || 0, status: card?.status || 'active', userId: card?.user?.id || '', departmentId: card?.user?.departmentId || '', positionId: card?.user?.positionId || '', memo: card?.memo || '', }; } interface CardDetailProps { card?: CardType; mode: 'create' | 'view' | 'edit'; isLoading?: boolean; } export function CardDetail({ card, mode: initialMode, isLoading }: CardDetailProps) { const router = useRouter(); const searchParams = useSearchParams(); const [mode, setMode] = useState(initialMode); const [isSaving, setIsSaving] = useState(false); const [isLoadingApproval, setIsLoadingApproval] = useState(false); const [employees, setEmployees] = useState>([]); const [fieldErrors, setFieldErrors] = useState({}); useEffect(() => { const urlMode = searchParams.get('mode'); if (urlMode === 'edit' && card) setMode('edit'); }, [searchParams, card]); // 직원 목록 로드 (수정/등록 모드) useEffect(() => { if (mode !== 'view') { getActiveEmployees().then(result => { if (result.success && result.data) setEmployees(result.data); }); } }, [mode]); const [formData, setFormData] = useState(() => buildFormData(card)); // card prop 변경 시 formData 동기화 (API 로딩 완료 후) useEffect(() => { if (card) { setFormData(buildFormData(card)); } }, [card]); const isViewMode = mode === 'view'; const isCreateMode = mode === 'create'; const handleChange = useCallback((field: keyof CardFormData, value: string | number) => { setFormData(prev => ({ ...prev, [field]: value })); const apiField = FORM_TO_API_FIELD[field] || field; setFieldErrors(prev => { if (!prev[apiField]) return prev; const next = { ...prev }; delete next[apiField]; return next; }); }, []); // API 필드명으로 에러 메시지 조회 const getError = (apiField: string) => fieldErrors[apiField]?.[0]; const handleBack = () => { router.push('/ko/hr/card-management'); }; const handleSubmit = async () => { if (!formData.cardCompany) { setFieldErrors({ card_company: ['카드사를 선택해주세요.'] }); toast.error('카드사를 선택해주세요.'); return; } setFieldErrors({}); setIsSaving(true); try { const result = isCreateMode ? await createCard(formData) : card?.id ? await updateCard(card.id, formData) : null; if (!result) return; if (result.success) { toast.success(isCreateMode ? '카드가 등록되었습니다.' : '카드가 수정되었습니다.'); router.push('/ko/hr/card-management'); } else { if (result.fieldErrors) { setFieldErrors(result.fieldErrors); } toast.error(result.error || (isCreateMode ? '카드 등록에 실패했습니다.' : '카드 수정에 실패했습니다.')); } } catch { toast.error('저장 중 오류가 발생했습니다.'); } finally { setIsSaving(false); } }; const deleteDialog = useDeleteDialog({ onDelete: async (id) => deleteCard(id), onSuccess: () => router.push('/ko/hr/card-management'), entityName: '카드', }); const handleCancel = () => { setFieldErrors({}); if (isCreateMode) { router.push('/ko/hr/card-management'); } else { setMode('view'); setFormData(buildFormData(card)); } }; const handleEdit = () => { setMode('edit'); if (card?.id) { router.push(`/ko/hr/card-management/${card.id}?mode=edit`); } }; const handleApprovalForm = async () => { if (!card?.id) return; setIsLoadingApproval(true); try { const result = await getApprovalFormUrl(card.id); if (result.success && result.data?.url) { window.open(result.data.url, '_blank'); } else { toast.error(result.error || '품의서 작성 페이지 URL 조회에 실패했습니다.'); } } catch { toast.error('품의서 작성 URL 조회 중 오류가 발생했습니다.'); } finally { setIsLoadingApproval(false); } }; if (isLoading) { return ( ); } // ===== 뷰 모드 ===== if (isViewMode) { return (
{/* 기본 정보 */} 기본 정보
카드사
{getCardCompanyLabel(card?.cardCompany || '')}
종류
{getCardTypeLabel(card?.cardType || '')}
카드번호
{card?.cardNumber ? formatCardNumber(card.cardNumber) : '-'}
카드명
{card?.cardName || '-'}
카드 별칭
{card?.alias || '-'}
유효기간(15/05)
{formatExpiryDate(card?.expiryDate || '')}
CSV
{card?.csv || '-'}
결제일
{getPaymentDayLabel(card?.paymentDay || '')}
총 한도
{formatCurrency(card?.totalLimit || 0)}
사용 금액
{formatCurrency(card?.usedAmount || 0)}
잔여한도
{formatCurrency(card?.remainingLimit || 0)}
상태
{CARD_STATUS_LABELS[card?.status || 'active']}
{/* 사용자 정보 */} 사용자 정보
부서
{card?.user?.departmentName || '-'}
사용자
{card?.user?.employeeName || '-'}
직책
{card?.user?.positionName || '-'}
메모
{card?.memo || '-'}
{/* 선결제 신청 */} 선결제 신청

한도를 초과한 월 경우 품의서를 작성해서 승인을 요청하세요

{/* 하단 버튼 */}
카드를 정말 삭제하시겠습니까?
삭제된 카드 정보는 복구할 수 없습니다. } onConfirm={deleteDialog.single.confirm} loading={deleteDialog.isPending} />
); } // ===== 생성/수정 모드 ===== return (
{/* 기본 정보 */} 기본 정보 {/* Row 1: 카드사 | 종류 | 카드번호 | 카드명 */}
{getError('card_company') &&

{getError('card_company')}

}
{getError('card_type') &&

{getError('card_type')}

}
handleChange('cardNumber', v)} /> {getError('card_number') &&

{getError('card_number')}

}
handleChange('cardName', e.target.value)} placeholder="카드명" /> {getError('card_name') &&

{getError('card_name')}

}
{/* Row 2: 카드 별칭 | 유효기간 | CSV | 결제일 */}
handleChange('alias', e.target.value)} placeholder="별칭" /> {getError('alias') &&

{getError('alias')}

}
handleChange('expiryDate', e.target.value)} placeholder="MMYY" maxLength={4} /> {getError('expiry_date') &&

{getError('expiry_date')}

}
handleChange('csv', e.target.value)} placeholder="CSV" maxLength={4} /> {getError('csv') &&

{getError('csv')}

}
{getError('payment_day') &&

{getError('payment_day')}

}
{/* Row 3: 총 한도 | 사용 금액 | 잔여한도 | 상태 */}
handleChange('totalLimit', Number(e.target.value) || 0)} placeholder="0" /> {getError('total_limit') &&

{getError('total_limit')}

}
handleChange('usedAmount', Number(e.target.value) || 0)} placeholder="0" /> {getError('used_amount') &&

{getError('used_amount')}

}
handleChange('remainingLimit', Number(e.target.value) || 0)} placeholder="0" /> {getError('remaining_limit') &&

{getError('remaining_limit')}

}
{getError('status') &&

{getError('status')}

}
{/* 사용자 정보 */} 사용자 정보
{getError('assigned_user_id') &&

{getError('assigned_user_id')}

}