feat:법인카드 서버 저장 기능 구현
- CorporateCard 모델 추가 - CorporateCardController API 추가 (CRUD) - 라우트 추가 (list, store, update, deactivate, destroy) - React 컴포넌트 API 연동 (fetch 호출) - 로딩 상태 UI 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
182
app/Http/Controllers/Finance/CorporateCardController.php
Normal file
182
app/Http/Controllers/Finance/CorporateCardController.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Finance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Finance\CorporateCard;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CorporateCardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 카드 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
$cards = CorporateCard::forTenant($tenantId)
|
||||
->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' => '카드가 영구 삭제되었습니다.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
52
app/Models/Finance/CorporateCard.php
Normal file
52
app/Models/Finance/CorporateCard.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Finance;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CorporateCard extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'corporate_cards';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'card_name',
|
||||
'card_company',
|
||||
'card_number',
|
||||
'card_type',
|
||||
'payment_day',
|
||||
'credit_limit',
|
||||
'current_usage',
|
||||
'card_holder_name',
|
||||
'actual_user',
|
||||
'expiry_date',
|
||||
'cvc',
|
||||
'status',
|
||||
'memo',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'payment_day' => '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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredCards.length === 0 && (
|
||||
{loading && (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<div className="animate-spin w-8 h-8 border-4 border-violet-500 border-t-transparent rounded-full mx-auto mb-4"></div>
|
||||
<p>카드 목록을 불러오는 중...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && filteredCards.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<CreditCard className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>등록된 카드가 없습니다.</p>
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user