diff --git a/app/Http/Controllers/Finance/CorporateCardController.php b/app/Http/Controllers/Finance/CorporateCardController.php new file mode 100644 index 00000000..148cdd1c --- /dev/null +++ b/app/Http/Controllers/Finance/CorporateCardController.php @@ -0,0 +1,182 @@ +orderBy('created_at', 'desc') + ->get() + ->map(function ($card) { + return [ + 'id' => $card->id, + 'cardName' => $card->card_name, + 'cardCompany' => $card->card_company, + 'cardNumber' => $card->card_number, + 'cardType' => $card->card_type, + 'paymentDay' => $card->payment_day, + 'creditLimit' => (float) $card->credit_limit, + 'currentUsage' => (float) $card->current_usage, + 'cardHolderName' => $card->card_holder_name, + 'actualUser' => $card->actual_user, + 'expiryDate' => $card->expiry_date, + 'cvc' => $card->cvc, + 'status' => $card->status, + 'memo' => $card->memo, + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => $cards, + ]); + } + + /** + * 카드 등록 + */ + public function store(Request $request): JsonResponse + { + $request->validate([ + 'cardName' => 'required|string|max:100', + 'cardCompany' => 'required|string|max:50', + 'cardNumber' => 'required|string|max:30', + 'cardType' => 'required|in:credit,debit', + 'cardHolderName' => 'required|string|max:100', + 'actualUser' => 'required|string|max:100', + ]); + + $tenantId = session('selected_tenant_id', 1); + + $card = CorporateCard::create([ + 'tenant_id' => $tenantId, + 'card_name' => $request->input('cardName'), + 'card_company' => $request->input('cardCompany'), + 'card_number' => $request->input('cardNumber'), + 'card_type' => $request->input('cardType'), + 'payment_day' => $request->input('paymentDay', 15), + 'credit_limit' => $request->input('creditLimit', 0), + 'current_usage' => 0, + 'card_holder_name' => $request->input('cardHolderName'), + 'actual_user' => $request->input('actualUser'), + 'expiry_date' => $request->input('expiryDate'), + 'cvc' => $request->input('cvc'), + 'status' => $request->input('status', 'active'), + 'memo' => $request->input('memo'), + ]); + + return response()->json([ + 'success' => true, + 'message' => '카드가 등록되었습니다.', + 'data' => [ + 'id' => $card->id, + 'cardName' => $card->card_name, + 'cardCompany' => $card->card_company, + 'cardNumber' => $card->card_number, + 'cardType' => $card->card_type, + 'paymentDay' => $card->payment_day, + 'creditLimit' => (float) $card->credit_limit, + 'currentUsage' => (float) $card->current_usage, + 'cardHolderName' => $card->card_holder_name, + 'actualUser' => $card->actual_user, + 'expiryDate' => $card->expiry_date, + 'cvc' => $card->cvc, + 'status' => $card->status, + 'memo' => $card->memo, + ], + ]); + } + + /** + * 카드 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $card = CorporateCard::findOrFail($id); + + $request->validate([ + 'cardName' => 'required|string|max:100', + 'cardCompany' => 'required|string|max:50', + 'cardNumber' => 'required|string|max:30', + 'cardType' => 'required|in:credit,debit', + 'cardHolderName' => 'required|string|max:100', + 'actualUser' => 'required|string|max:100', + ]); + + $card->update([ + 'card_name' => $request->input('cardName'), + 'card_company' => $request->input('cardCompany'), + 'card_number' => $request->input('cardNumber'), + 'card_type' => $request->input('cardType'), + 'payment_day' => $request->input('paymentDay', 15), + 'credit_limit' => $request->input('creditLimit', 0), + 'card_holder_name' => $request->input('cardHolderName'), + 'actual_user' => $request->input('actualUser'), + 'expiry_date' => $request->input('expiryDate'), + 'cvc' => $request->input('cvc'), + 'status' => $request->input('status', 'active'), + 'memo' => $request->input('memo'), + ]); + + return response()->json([ + 'success' => true, + 'message' => '카드가 수정되었습니다.', + 'data' => [ + 'id' => $card->id, + 'cardName' => $card->card_name, + 'cardCompany' => $card->card_company, + 'cardNumber' => $card->card_number, + 'cardType' => $card->card_type, + 'paymentDay' => $card->payment_day, + 'creditLimit' => (float) $card->credit_limit, + 'currentUsage' => (float) $card->current_usage, + 'cardHolderName' => $card->card_holder_name, + 'actualUser' => $card->actual_user, + 'expiryDate' => $card->expiry_date, + 'cvc' => $card->cvc, + 'status' => $card->status, + 'memo' => $card->memo, + ], + ]); + } + + /** + * 카드 비활성화 + */ + public function deactivate(int $id): JsonResponse + { + $card = CorporateCard::findOrFail($id); + $card->update(['status' => 'inactive']); + + return response()->json([ + 'success' => true, + 'message' => '카드가 비활성화되었습니다.', + ]); + } + + /** + * 카드 영구삭제 + */ + public function destroy(int $id): JsonResponse + { + $card = CorporateCard::findOrFail($id); + $card->forceDelete(); + + return response()->json([ + 'success' => true, + 'message' => '카드가 영구 삭제되었습니다.', + ]); + } +} diff --git a/app/Models/Finance/CorporateCard.php b/app/Models/Finance/CorporateCard.php new file mode 100644 index 00000000..5faf5ba6 --- /dev/null +++ b/app/Models/Finance/CorporateCard.php @@ -0,0 +1,52 @@ + 'integer', + 'credit_limit' => 'decimal:2', + 'current_usage' => 'decimal:2', + ]; + + /** + * 활성 카드만 조회 + */ + public function scopeActive($query) + { + return $query->where('status', 'active'); + } + + /** + * 특정 테넌트의 카드 조회 + */ + public function scopeForTenant($query, $tenantId) + { + return $query->where('tenant_id', $tenantId); + } +} diff --git a/resources/views/finance/corporate-cards.blade.php b/resources/views/finance/corporate-cards.blade.php index 4d97401f..8d9ecfb0 100644 --- a/resources/views/finance/corporate-cards.blade.php +++ b/resources/views/finance/corporate-cards.blade.php @@ -51,8 +51,9 @@ const Zap = createIcon('zap'); function CorporateCardsManagement() { - // 카드 목록 데이터 (빈 배열로 시작 - 실제 데이터는 서버 연동 후 로드) + // 카드 목록 데이터 const [cards, setCards] = useState([]); + const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [filterStatus, setFilterStatus] = useState('all'); @@ -60,6 +61,9 @@ function CorporateCardsManagement() { const [modalMode, setModalMode] = useState('add'); // 'add' or 'edit' const [editingCard, setEditingCard] = useState(null); + // CSRF 토큰 + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); + // 새 카드 폼 초기값 const initialFormState = { cardName: '', @@ -77,8 +81,28 @@ function CorporateCardsManagement() { }; const [formData, setFormData] = useState(initialFormState); + // 초기 데이터 로드 + useEffect(() => { + fetchCards(); + }, []); + + const fetchCards = async () => { + try { + setLoading(true); + const response = await fetch('/finance/corporate-cards/list'); + const result = await response.json(); + if (result.success) { + setCards(result.data); + } + } catch (error) { + console.error('카드 목록 로드 실패:', error); + } finally { + setLoading(false); + } + }; + // 테스트용 임시 데이터 생성 - const generateTestData = () => { + const generateTestData = async () => { const companies = ['삼성카드', '현대카드', '국민카드', '신한카드', '롯데카드']; const names = ['업무용', '마케팅', '개발팀', '영업팀', '관리팀']; const users = ['김철수', '이영희', '박민수', '최지영', '정대한']; @@ -87,24 +111,40 @@ function CorporateCardsManagement() { const randomCard = () => `${randomNum(1000,9999)}-${randomNum(1000,9999)}-${randomNum(1000,9999)}-${randomNum(1000,9999)}`; const randomExpiry = () => `${randomNum(25,30)}/${String(randomNum(1,12)).padStart(2,'0')}`; - const newCards = Array.from({ length: 3 }, (_, i) => ({ - id: Date.now() + i, - cardName: `${names[randomNum(0,4)]} 법인카드`, - cardCompany: companies[randomNum(0,4)], - cardNumber: randomCard(), - cardType: Math.random() > 0.3 ? 'credit' : 'debit', - paymentDay: [10, 15, 20, 25][randomNum(0,3)], - creditLimit: randomNum(3, 20) * 1000000, - currentUsage: randomNum(0, 10) * 100000, - cardHolderName: '(주)테스트회사', - actualUser: users[randomNum(0,4)], - expiryDate: randomExpiry(), - cvc: String(randomNum(100,999)), - status: 'active', - memo: '테스트 데이터' - })); + // 3개의 테스트 카드를 서버에 저장 + for (let i = 0; i < 3; i++) { + const testCard = { + cardName: `${names[randomNum(0,4)]} 법인카드`, + cardCompany: companies[randomNum(0,4)], + cardNumber: randomCard(), + cardType: Math.random() > 0.3 ? 'credit' : 'debit', + paymentDay: [10, 15, 20, 25][randomNum(0,3)], + creditLimit: randomNum(3, 20) * 1000000, + cardHolderName: '(주)테스트회사', + actualUser: users[randomNum(0,4)], + expiryDate: randomExpiry(), + cvc: String(randomNum(100,999)), + status: 'active', + memo: '테스트 데이터' + }; - setCards(prev => [...prev, ...newCards]); + try { + const response = await fetch('/finance/corporate-cards/store', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + }, + body: JSON.stringify(testCard), + }); + const result = await response.json(); + if (result.success) { + setCards(prev => [...prev, result.data]); + } + } catch (error) { + console.error('테스트 데이터 생성 실패:', error); + } + } }; // 카드사 목록 @@ -174,52 +214,111 @@ function CorporateCardsManagement() { }; // 카드 저장 - const handleSaveCard = () => { + const handleSaveCard = async () => { if (!formData.cardName || !formData.cardNumber || !formData.cardHolderName || !formData.actualUser) { 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 - )); - } + try { + if (modalMode === 'add') { + const response = await fetch('/finance/corporate-cards/store', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + }, + body: JSON.stringify({ + ...formData, + creditLimit: parseInt(formData.creditLimit) || 0, + }), + }); + const result = await response.json(); + if (result.success) { + setCards(prev => [...prev, result.data]); + } else { + alert(result.message || '저장에 실패했습니다.'); + return; + } + } else { + const response = await fetch(`/finance/corporate-cards/${editingCard.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + }, + body: JSON.stringify({ + ...formData, + creditLimit: parseInt(formData.creditLimit) || 0, + }), + }); + const result = await response.json(); + if (result.success) { + setCards(prev => prev.map(card => + card.id === editingCard.id ? result.data : card + )); + } else { + alert(result.message || '수정에 실패했습니다.'); + return; + } + } - setShowModal(false); - setEditingCard(null); + setShowModal(false); + setEditingCard(null); + } catch (error) { + console.error('저장 실패:', error); + alert('저장 중 오류가 발생했습니다.'); + } }; // 카드 비활성화 (소프트 삭제) - const handleDeactivateCard = (id) => { + const handleDeactivateCard = async (id) => { if (confirm('카드를 비활성화하시겠습니까?\n(목록에서 숨겨지지만 데이터는 유지됩니다)')) { - setCards(prev => prev.map(card => - card.id === id ? { ...card, status: 'inactive' } : card - )); - if (showModal) { - setShowModal(false); - setEditingCard(null); + try { + const response = await fetch(`/finance/corporate-cards/${id}/deactivate`, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': csrfToken, + }, + }); + const result = await response.json(); + if (result.success) { + setCards(prev => prev.map(card => + card.id === id ? { ...card, status: 'inactive' } : card + )); + if (showModal) { + setShowModal(false); + setEditingCard(null); + } + } + } catch (error) { + console.error('비활성화 실패:', error); + alert('비활성화 중 오류가 발생했습니다.'); } } }; // 카드 영구삭제 - const handlePermanentDeleteCard = (id) => { + const handlePermanentDeleteCard = async (id) => { if (confirm('⚠️ 카드를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.')) { - setCards(prev => prev.filter(card => card.id !== id)); - if (showModal) { - setShowModal(false); - setEditingCard(null); + try { + const response = await fetch(`/finance/corporate-cards/${id}`, { + method: 'DELETE', + headers: { + 'X-CSRF-TOKEN': csrfToken, + }, + }); + const result = await response.json(); + if (result.success) { + setCards(prev => prev.filter(card => card.id !== id)); + if (showModal) { + setShowModal(false); + setEditingCard(null); + } + } + } catch (error) { + console.error('삭제 실패:', error); + alert('삭제 중 오류가 발생했습니다.'); } } }; @@ -426,7 +525,14 @@ className={`h-1.5 rounded-full ${getUsageColor(getUsagePercent(card.currentUsage ))} - {filteredCards.length === 0 && ( + {loading && ( +
카드 목록을 불러오는 중...
+등록된 카드가 없습니다.
diff --git a/routes/web.php b/routes/web.php index c36086ba..76c2e18e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -690,6 +690,16 @@ return view('finance.corporate-cards'); })->name('corporate-cards'); + + // 법인카드 API + Route::prefix('corporate-cards')->name('corporate-cards.')->group(function () { + Route::get('/list', [\App\Http\Controllers\Finance\CorporateCardController::class, 'index'])->name('list'); + Route::post('/store', [\App\Http\Controllers\Finance\CorporateCardController::class, 'store'])->name('store'); + Route::put('/{id}', [\App\Http\Controllers\Finance\CorporateCardController::class, 'update'])->name('update'); + Route::post('/{id}/deactivate', [\App\Http\Controllers\Finance\CorporateCardController::class, 'deactivate'])->name('deactivate'); + Route::delete('/{id}', [\App\Http\Controllers\Finance\CorporateCardController::class, 'destroy'])->name('destroy'); + }); + Route::get('/card-transactions', function () { if (request()->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('finance.card-transactions'));