'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 { CardNumberInput } from '@/components/ui/card-number-input'; import { formatCardNumber } from '@/lib/formatters'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 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 { FormField } from '@/components/molecules/FormField'; 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 { cardFormSchema } 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, } 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 [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 () => { const validation = cardFormSchema.safeParse(formData); if (!validation.success) { const zodErrors: FieldErrors = {}; for (const issue of validation.error.issues) { const field = String(issue.path[0]); const apiField = FORM_TO_API_FIELD[field] || field; if (!zodErrors[apiField]) zodErrors[apiField] = []; zodErrors[apiField].push(issue.message); } setFieldErrors(zodErrors); const firstMsg = validation.error.issues[0]?.message || '입력값을 확인해주세요.'; toast.error(firstMsg); 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 = () => { router.push('/ko/approval/draft/new'); }; 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: 카드사 | 종류 | 카드번호 | 카드명 */}
handleChange('cardCompany', v)} options={[...CARD_COMPANIES]} selectPlaceholder="카드사 선택" error={getError('card_company')} /> handleChange('cardType', v === '_none' ? '' : v)} options={[{ value: '_none', label: '선택 안함' }, ...CARD_TYPE_OPTIONS]} selectPlaceholder="종류 선택" error={getError('card_type')} /> handleChange('cardNumber', v)} /> handleChange('cardName', v)} placeholder="카드명" error={getError('card_name')} />
{/* Row 2: 카드 별칭 | 유효기간 | CSV | 결제일 */}
handleChange('alias', v)} placeholder="별칭" error={getError('alias')} /> handleChange('expiryDate', v)} placeholder="MMYY" maxLength={4} error={getError('expiry_date')} /> handleChange('csv', v)} placeholder="CSV" maxLength={4} error={getError('csv')} /> handleChange('paymentDay', v === '_none' ? '' : v)} options={[{ value: '_none', label: '선택 안함' }, ...PAYMENT_DAY_OPTIONS]} selectPlaceholder="결제일 선택" error={getError('payment_day')} />
{/* Row 3: 총 한도 | 사용 금액 | 잔여한도 | 상태 */}
handleChange('totalLimit', v ?? 0)} placeholder="0" error={getError('total_limit')} /> handleChange('usedAmount', v ?? 0)} placeholder="0" error={getError('used_amount')} /> handleChange('remainingLimit', v ?? 0)} placeholder="0" error={getError('remaining_limit')} /> handleChange('status', v as CardStatus)} options={[ { value: 'active', label: CARD_STATUS_LABELS.active }, { value: 'suspended', label: CARD_STATUS_LABELS.suspended }, ]} selectPlaceholder="상태 선택" error={getError('status')} />
{/* 사용자 정보 */} 사용자 정보
handleChange('userId', v === '_none' ? '' : v)} options={[{ value: '_none', label: '미지정' }, ...employees.map(e => ({ value: e.id, label: e.label }))]} selectPlaceholder="사용자 선택" error={getError('assigned_user_id')} className="md:col-span-2" /> handleChange('memo', v)} placeholder="메모" rows={2} error={getError('memo')} className="md:col-span-2" />
{/* 하단 버튼 */}
); }