Files
sam-manage/resources/views/finance/corporate-cards.blade.php

601 lines
29 KiB
PHP

@extends('layouts.app')
@section('title', '법인카드 등록/조회')
@push('styles')
<style>
@media print {
.no-print { display: none !important; }
}
</style>
@endpush
@section('content')
<div id="corporate-cards-root"></div>
@endsection
@push('scripts')
<script src="https://unpkg.com/react@18/umd/react.development.js?v={{ time() }}"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js?v={{ time() }}"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js?v={{ time() }}"></script>
<script src="https://unpkg.com/lucide@latest?v={{ time() }}"></script>
@verbatim
<script type="text/babel">
const { useState, useRef, useEffect } = React;
// Lucide 아이콘 래핑
const createIcon = (name) => ({ className = "w-5 h-5", ...props }) => {
const ref = useRef(null);
useEffect(() => {
if (ref.current && lucide.icons[name]) {
ref.current.innerHTML = '';
const svg = lucide.createElement(lucide.icons[name]);
svg.setAttribute('class', className);
ref.current.appendChild(svg);
}
}, [className]);
return <span ref={ref} className="inline-flex items-center" {...props} />;
};
const CreditCard = createIcon('credit-card');
const Plus = createIcon('plus');
const Search = createIcon('search');
const Edit = createIcon('edit');
const Trash2 = createIcon('trash-2');
const X = createIcon('x');
const Building = createIcon('building');
const Calendar = createIcon('calendar');
const DollarSign = createIcon('dollar-sign');
const CheckCircle = createIcon('check-circle');
const XCircle = createIcon('x-circle');
function CorporateCardsManagement() {
// 카드 목록 데이터
const [cards, setCards] = useState([
{
id: 1,
cardName: '업무용 법인카드',
cardCompany: '삼성카드',
cardNumber: '9410-1234-5678-9012',
cardType: 'credit',
paymentDay: 15,
creditLimit: 10000000,
currentUsage: 3500000,
holder: '김대표',
status: 'active',
memo: '일반 업무용'
},
{
id: 2,
cardName: '마케팅 법인카드',
cardCompany: '현대카드',
cardNumber: '5412-9876-5432-1098',
cardType: 'credit',
paymentDay: 25,
creditLimit: 5000000,
currentUsage: 2100000,
holder: '박마케팅',
status: 'active',
memo: '마케팅/광고비 전용'
},
{
id: 3,
cardName: '개발팀 체크카드',
cardCompany: '국민카드',
cardNumber: '4532-1111-2222-3333',
cardType: 'debit',
paymentDay: 0,
creditLimit: 0,
currentUsage: 850000,
holder: '이개발',
status: 'active',
memo: '개발 장비/소프트웨어 구매'
},
{
id: 4,
cardName: '예비 법인카드',
cardCompany: '신한카드',
cardNumber: '9876-5555-4444-3333',
cardType: 'credit',
paymentDay: 10,
creditLimit: 3000000,
currentUsage: 0,
holder: '최관리',
status: 'inactive',
memo: '비상용'
}
]);
const [searchTerm, setSearchTerm] = useState('');
const [filterStatus, setFilterStatus] = useState('all');
const [showModal, setShowModal] = useState(false);
const [modalMode, setModalMode] = useState('add'); // 'add' or 'edit'
const [editingCard, setEditingCard] = useState(null);
// 새 카드 폼 초기값
const initialFormState = {
cardName: '',
cardCompany: '삼성카드',
cardNumber: '',
cardType: 'credit',
paymentDay: 15,
creditLimit: '',
holder: '',
status: 'active',
memo: ''
};
const [formData, setFormData] = useState(initialFormState);
// 카드사 목록
const cardCompanies = ['삼성카드', '현대카드', '국민카드', '신한카드', '롯데카드', 'BC카드', '하나카드', '우리카드', 'NH농협카드'];
// 금액 포맷
const formatCurrency = (num) => {
if (!num) return '0';
return num.toLocaleString();
};
// 입력용 금액 포맷 (3자리 콤마)
const formatInputCurrency = (value) => {
if (!value && value !== 0) return '';
const num = String(value).replace(/[^\d]/g, '');
if (!num) return '';
return Number(num).toLocaleString();
};
// 입력값에서 숫자만 추출
const parseInputCurrency = (value) => {
return String(value).replace(/[^\d]/g, '');
};
// 카드번호 포맷
const formatCardNumber = (num) => {
if (!num) return '';
return num.replace(/(.{4})/g, '$1-').slice(0, -1);
};
// 필터링된 카드 목록
const filteredCards = cards.filter(card => {
const matchesSearch = card.cardName.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.cardCompany.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.holder.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = filterStatus === 'all' || card.status === filterStatus;
return matchesSearch && matchesStatus;
});
// 카드 추가 모달 열기
const handleAddCard = () => {
setModalMode('add');
setFormData(initialFormState);
setShowModal(true);
};
// 카드 수정 모달 열기
const handleEditCard = (card) => {
setModalMode('edit');
setEditingCard(card);
setFormData({
cardName: card.cardName,
cardCompany: card.cardCompany,
cardNumber: card.cardNumber,
cardType: card.cardType,
paymentDay: card.paymentDay,
creditLimit: card.creditLimit,
holder: card.holder,
status: card.status,
memo: card.memo
});
setShowModal(true);
};
// 카드 저장
const handleSaveCard = () => {
if (!formData.cardName || !formData.cardNumber || !formData.holder) {
alert('필수 항목을 입력해주세요.');
return;
}
if (modalMode === 'add') {
const newCard = {
id: Date.now(),
...formData,
creditLimit: parseInt(formData.creditLimit) || 0,
currentUsage: 0
};
setCards(prev => [...prev, newCard]);
} else {
setCards(prev => prev.map(card =>
card.id === editingCard.id
? { ...card, ...formData, creditLimit: parseInt(formData.creditLimit) || 0 }
: card
));
}
setShowModal(false);
setEditingCard(null);
};
// 카드 삭제
const handleDeleteCard = (id) => {
if (confirm('정말 삭제하시겠습니까?')) {
setCards(prev => prev.filter(card => card.id !== id));
if (showModal) {
setShowModal(false);
setEditingCard(null);
}
}
};
// 사용률 계산
const getUsagePercent = (usage, limit) => {
if (!limit) return 0;
return Math.round((usage / limit) * 100);
};
// 사용률 색상
const getUsageColor = (percent) => {
if (percent >= 80) return 'bg-red-500';
if (percent >= 50) return 'bg-yellow-500';
return 'bg-emerald-500';
};
// 총 한도 및 사용액
const totalLimit = cards.filter(c => c.status === 'active' && c.cardType === 'credit').reduce((sum, c) => sum + c.creditLimit, 0);
const totalUsage = cards.filter(c => c.status === 'active').reduce((sum, c) => sum + c.currentUsage, 0);
return (
<div className="bg-gray-50 min-h-screen">
{/* 헤더 */}
<header className="bg-white border-b border-gray-200 rounded-t-xl mb-6">
<div className="px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="p-2 bg-violet-100 rounded-xl">
<CreditCard className="w-6 h-6 text-violet-600" />
</div>
<div>
<h1 className="text-xl font-bold text-gray-900">법인카드 등록/조회</h1>
<p className="text-sm text-gray-500">Corporate Card Management</p>
</div>
</div>
<button
onClick={handleAddCard}
className="flex items-center gap-2 px-4 py-2 bg-violet-600 hover:bg-violet-700 text-white rounded-lg transition-colors"
>
<Plus className="w-4 h-4" />
<span className="text-sm font-medium">카드 등록</span>
</button>
</div>
</div>
</header>
{/* 요약 카드 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500">등록 카드</span>
<CreditCard className="w-5 h-5 text-gray-400" />
</div>
<p className="text-2xl font-bold text-gray-900">{cards.length}</p>
<p className="text-xs text-gray-400 mt-1">활성 {cards.filter(c => c.status === 'active').length}</p>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500"> 한도</span>
<DollarSign className="w-5 h-5 text-gray-400" />
</div>
<p className="text-2xl font-bold text-gray-900">{formatCurrency(totalLimit)}</p>
<p className="text-xs text-gray-400 mt-1">신용카드 기준</p>
</div>
<div className="bg-white rounded-xl border border-violet-200 p-6 bg-violet-50/30">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-violet-700">이번 사용</span>
<CreditCard className="w-5 h-5 text-violet-500" />
</div>
<p className="text-2xl font-bold text-violet-600">{formatCurrency(totalUsage)}</p>
<p className="text-xs text-violet-500 mt-1">{totalLimit > 0 ? getUsagePercent(totalUsage, totalLimit) : 0}% 사용</p>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500">잔여 한도</span>
<DollarSign className="w-5 h-5 text-emerald-500" />
</div>
<p className="text-2xl font-bold text-emerald-600">{formatCurrency(totalLimit - totalUsage)}</p>
<p className="text-xs text-gray-400 mt-1">사용 가능</p>
</div>
</div>
{/* 검색 및 필터 */}
<div className="bg-white rounded-xl border border-gray-200 p-4 mb-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="카드명, 카드사, 사용자 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 focus:border-violet-500"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => setFilterStatus('all')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
filterStatus === 'all' ? 'bg-violet-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
전체
</button>
<button
onClick={() => setFilterStatus('active')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
filterStatus === 'active' ? 'bg-emerald-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
활성
</button>
<button
onClick={() => setFilterStatus('inactive')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
filterStatus === 'inactive' ? 'bg-gray-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
비활성
</button>
</div>
</div>
</div>
{/* 카드 목록 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredCards.map(card => (
<div
key={card.id}
onClick={() => handleEditCard(card)}
className={`bg-white rounded-xl border-2 p-6 cursor-pointer transition-all hover:shadow-lg ${
card.status === 'active' ? 'border-gray-200 hover:border-violet-300' : 'border-gray-200 opacity-60'
}`}
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${card.status === 'active' ? 'bg-violet-100' : 'bg-gray-100'}`}>
<CreditCard className={`w-5 h-5 ${card.status === 'active' ? 'text-violet-600' : 'text-gray-400'}`} />
</div>
<div>
<h3 className="font-bold text-gray-900">{card.cardName}</h3>
<p className="text-sm text-gray-500">{card.cardCompany}</p>
</div>
</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
card.status === 'active' ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-100 text-gray-500'
}`}>
{card.status === 'active' ? '활성' : '비활성'}
</span>
</div>
<div className="mb-4">
<p className="text-lg font-mono text-gray-700 tracking-wider">{card.cardNumber}</p>
</div>
<div className="grid grid-cols-2 gap-4 mb-4 text-sm">
<div>
<p className="text-gray-500">사용자</p>
<p className="font-medium text-gray-900">{card.holder}</p>
</div>
<div>
<p className="text-gray-500">카드종류</p>
<p className="font-medium text-gray-900">{card.cardType === 'credit' ? '신용카드' : '체크카드'}</p>
</div>
{card.cardType === 'credit' && (
<>
<div>
<p className="text-gray-500">결제일</p>
<p className="font-medium text-gray-900">매월 {card.paymentDay}</p>
</div>
<div>
<p className="text-gray-500">한도</p>
<p className="font-medium text-gray-900">{formatCurrency(card.creditLimit)}</p>
</div>
</>
)}
</div>
{card.cardType === 'credit' && card.creditLimit > 0 && (
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500">사용현황</span>
<span className="text-sm font-medium text-gray-900">
{formatCurrency(card.currentUsage)} / {formatCurrency(card.creditLimit)}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${getUsageColor(getUsagePercent(card.currentUsage, card.creditLimit))}`}
style={{ width: `${Math.min(getUsagePercent(card.currentUsage, card.creditLimit), 100)}%` }}
></div>
</div>
<p className="text-xs text-gray-400 mt-1 text-right">{getUsagePercent(card.currentUsage, card.creditLimit)}% 사용</p>
</div>
)}
{card.memo && (
<p className="text-xs text-gray-400 mt-3 pt-3 border-t border-gray-100">{card.memo}</p>
)}
</div>
))}
{filteredCards.length === 0 && (
<div className="col-span-full text-center py-12 text-gray-400">
<CreditCard className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>등록된 카드가 없습니다.</p>
</div>
)}
</div>
{/* 등록/수정 모달 */}
{showModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl p-6 w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-gray-900">
{modalMode === 'add' ? '법인카드 등록' : '법인카드 수정'}
</h3>
<button
onClick={() => { setShowModal(false); setEditingCard(null); }}
className="p-1 hover:bg-gray-100 rounded-lg"
>
<X className="w-5 h-5 text-gray-500" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">카드명 *</label>
<input
type="text"
value={formData.cardName}
onChange={(e) => setFormData(prev => ({ ...prev, cardName: e.target.value }))}
placeholder="예: 업무용 법인카드"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">카드사</label>
<select
value={formData.cardCompany}
onChange={(e) => setFormData(prev => ({ ...prev, cardCompany: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500"
>
{cardCompanies.map(company => (
<option key={company} value={company}>{company}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">카드종류</label>
<select
value={formData.cardType}
onChange={(e) => setFormData(prev => ({ ...prev, cardType: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500"
>
<option value="credit">신용카드</option>
<option value="debit">체크카드</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">카드번호 *</label>
<input
type="text"
value={formData.cardNumber}
onChange={(e) => setFormData(prev => ({ ...prev, cardNumber: e.target.value }))}
placeholder="1234-5678-9012-3456"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 font-mono"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">사용자 *</label>
<input
type="text"
value={formData.holder}
onChange={(e) => setFormData(prev => ({ ...prev, holder: e.target.value }))}
placeholder="카드 사용자명"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">상태</label>
<select
value={formData.status}
onChange={(e) => setFormData(prev => ({ ...prev, status: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500"
>
<option value="active">활성</option>
<option value="inactive">비활성</option>
</select>
</div>
</div>
{formData.cardType === 'credit' && (
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">결제일</label>
<select
value={formData.paymentDay}
onChange={(e) => setFormData(prev => ({ ...prev, paymentDay: parseInt(e.target.value) }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500"
>
{[1, 5, 10, 14, 15, 20, 25, 27].map(day => (
<option key={day} value={day}>매월 {day}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">사용한도</label>
<input
type="text"
value={formatInputCurrency(formData.creditLimit)}
onChange={(e) => setFormData(prev => ({ ...prev, creditLimit: parseInputCurrency(e.target.value) }))}
placeholder="0"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 text-right"
/>
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">메모</label>
<textarea
value={formData.memo}
onChange={(e) => setFormData(prev => ({ ...prev, memo: e.target.value }))}
placeholder="카드 용도나 특이사항..."
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 resize-none"
/>
</div>
</div>
<div className="flex gap-3 mt-6">
{modalMode === 'edit' && (
<button
onClick={() => handleDeleteCard(editingCard.id)}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2"
>
<span>🗑️</span> 삭제
</button>
)}
<button
onClick={() => { setShowModal(false); setEditingCard(null); }}
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 flex items-center justify-center gap-2"
>
<span></span> 취소
</button>
<button
onClick={handleSaveCard}
className="flex-1 px-4 py-2 bg-violet-600 hover:bg-violet-700 text-white rounded-lg flex items-center justify-center gap-2"
>
<span></span> {modalMode === 'add' ? '등록' : '저장'}
</button>
</div>
</div>
</div>
)}
</div>
);
}
// React 앱 마운트
const rootElement = document.getElementById('corporate-cards-root');
if (rootElement) {
ReactDOM.createRoot(rootElement).render(<CorporateCardsManagement />);
}
</script>
@endverbatim
@endpush