feat: [finance] 카드거래 표시 포맷 3개 화면 통일
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
use App\Models\Barobill\AccountCode;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AccountLedgerController extends Controller
|
||||
@@ -68,12 +69,16 @@ public function list(Request $request): JsonResponse
|
||||
'tp.biz_no',
|
||||
'jel.debit_amount',
|
||||
'jel.credit_amount',
|
||||
DB::raw("'journal' as source_type"),
|
||||
DB::raw("COALESCE(je.source_type, 'journal') as source_type"),
|
||||
'jel.journal_entry_id as source_id',
|
||||
'je.source_key',
|
||||
])
|
||||
->orderBy('je.entry_date')
|
||||
->get();
|
||||
|
||||
// 카드거래 상세 조회
|
||||
$cardTxMap = $this->fetchCardTransactions($tenantId, $allLines);
|
||||
|
||||
// 이월잔액 (일반전표만)
|
||||
$carryForward = $this->calculateCarryForward($tenantId, $accountCode, $startDate, $account->category);
|
||||
|
||||
@@ -100,16 +105,22 @@ public function list(Request $request): JsonResponse
|
||||
|
||||
$runningBalance += $isDebitNormal ? ($debit - $credit) : ($credit - $debit);
|
||||
|
||||
$cardTx = null;
|
||||
if ($line->source_type === 'ecard_transaction' && $line->source_key) {
|
||||
$cardTx = $cardTxMap[$line->source_key] ?? null;
|
||||
}
|
||||
|
||||
$monthlyData[$month]['items'][] = [
|
||||
'date' => $line->date,
|
||||
'description' => $line->description,
|
||||
'trading_partner_name' => $line->trading_partner_name,
|
||||
'biz_no' => $line->biz_no,
|
||||
'trading_partner_name' => $cardTx ? ($cardTx['merchant_name'] ?: $line->trading_partner_name) : $line->trading_partner_name,
|
||||
'biz_no' => $cardTx ? ($cardTx['merchant_biz_num'] ?: $line->biz_no) : $line->biz_no,
|
||||
'debit_amount' => $debit,
|
||||
'credit_amount' => $credit,
|
||||
'balance' => $runningBalance,
|
||||
'source_type' => $line->source_type,
|
||||
'source_id' => (int) $line->source_id,
|
||||
'card_tx' => $cardTx,
|
||||
];
|
||||
|
||||
$monthlyData[$month]['subtotal']['debit'] += $debit;
|
||||
@@ -141,6 +152,82 @@ public function list(Request $request): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드거래 상세 일괄 조회 (source_key 기반)
|
||||
*/
|
||||
private function fetchCardTransactions(int $tenantId, Collection $lines): array
|
||||
{
|
||||
$sourceKeys = $lines
|
||||
->filter(fn ($l) => $l->source_type === 'ecard_transaction' && $l->source_key)
|
||||
->pluck('source_key')
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (empty($sourceKeys)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// source_key = "card_num|use_dt|approval_num|approval_amount"
|
||||
$conditions = [];
|
||||
foreach ($sourceKeys as $key) {
|
||||
$parts = explode('|', $key);
|
||||
if (count($parts) === 4) {
|
||||
$conditions[] = $parts;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($conditions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = DB::table('barobill_card_transactions')
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
$query->where(function ($q) use ($conditions) {
|
||||
foreach ($conditions as $c) {
|
||||
$q->orWhere(function ($sub) use ($c) {
|
||||
$sub->where('card_num', $c[0])
|
||||
->where('use_dt', $c[1])
|
||||
->where('approval_num', $c[2])
|
||||
->whereRaw('CAST(approval_amount AS SIGNED) = ?', [(int) $c[3]]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$txs = $query->get();
|
||||
|
||||
$map = [];
|
||||
foreach ($txs as $tx) {
|
||||
$uniqueKey = implode('|', [
|
||||
$tx->card_num,
|
||||
$tx->use_dt,
|
||||
$tx->approval_num,
|
||||
(int) $tx->approval_amount,
|
||||
]);
|
||||
|
||||
$supplyAmount = $tx->modified_supply_amount !== null
|
||||
? (int) $tx->modified_supply_amount
|
||||
: (int) $tx->approval_amount - (int) $tx->tax;
|
||||
$taxAmount = $tx->modified_tax !== null
|
||||
? (int) $tx->modified_tax
|
||||
: (int) $tx->tax;
|
||||
|
||||
$map[$uniqueKey] = [
|
||||
'card_num' => $tx->card_num,
|
||||
'card_company_name' => $tx->card_company_name,
|
||||
'merchant_name' => $tx->merchant_name,
|
||||
'merchant_biz_num' => $tx->merchant_biz_num,
|
||||
'deduction_type' => $tx->deduction_type,
|
||||
'supply_amount' => $supplyAmount,
|
||||
'tax_amount' => $taxAmount,
|
||||
'approval_amount' => (int) $tx->approval_amount,
|
||||
];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이월잔액 계산 (일반전표만)
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
const Printer = createIcon('printer');
|
||||
const X = createIcon('x');
|
||||
const ExternalLink = createIcon('external-link');
|
||||
const CreditCard = createIcon('credit-card');
|
||||
|
||||
// 숫자 포맷
|
||||
const fmt = (n) => {
|
||||
@@ -122,6 +123,27 @@ function DetailModal({ detail, onClose }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 카드거래 정보 */}
|
||||
{detail.cardTx && (
|
||||
<div className="mb-4 p-3 bg-orange-50 border border-orange-200 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<CreditCard className="w-4 h-4 text-orange-500" />
|
||||
<span className="text-sm font-semibold text-orange-700">카드거래 정보</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${detail.cardTx.deduction_type === 'non_deductible' ? 'bg-red-100 text-red-600' : 'bg-emerald-100 text-emerald-600'}`}>
|
||||
{detail.cardTx.deduction_type === 'non_deductible' ? '불공제' : '공제'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
||||
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>카드번호</span><span className="font-mono">{'····' + detail.cardTx.card_num.slice(-4)}</span></div>
|
||||
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>카드사</span><span>{detail.cardTx.card_company_name}</span></div>
|
||||
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>가맹점</span><span>{detail.cardTx.merchant_name}</span></div>
|
||||
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>사업자번호</span><span className="font-mono">{detail.cardTx.merchant_biz_num || '-'}</span></div>
|
||||
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>공급가액</span><span className="text-blue-700">{fmt(detail.cardTx.supply_amount)}</span></div>
|
||||
<div className="flex"><span className="text-gray-500 shrink-0" style={{width: '80px'}}>세액</span><span className="text-red-600">{fmt(detail.cardTx.tax_amount)}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 분개 라인 테이블 */}
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
@@ -313,12 +335,12 @@ function AccountLedger() {
|
||||
|
||||
// 전표 드릴다운 (모달)
|
||||
const drillDown = (item) => {
|
||||
if (item.source_type === 'journal') {
|
||||
if (item.source_type === 'journal' || item.source_type === 'ecard_transaction' || item.source_type === 'bank_transaction') {
|
||||
fetch('/finance/journal-entries/' + item.source_id)
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success && res.data) {
|
||||
setDetail({ type: 'journal', data: res.data, sourceId: item.source_id });
|
||||
setDetail({ type: 'journal', data: res.data, sourceId: item.source_id, cardTx: item.card_tx || null });
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -446,9 +468,22 @@ className="flex items-center gap-1 px-4 py-1.5 bg-indigo-600 text-white text-sm
|
||||
<tr key={mi + '-' + idx} className={rowHover} onClick={() => drillDown(item)}>
|
||||
<td className={cellBorder + ' px-3 py-1.5 text-center text-gray-600'}>{item.date}</td>
|
||||
<td className={cellBorder + ' px-3 py-1.5'}>
|
||||
{item.description}
|
||||
{item.source_type === 'hometax' && (
|
||||
<span className="ml-1 text-xs text-orange-500">[홈택스]</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{item.card_tx && <CreditCard className="w-3.5 h-3.5 text-orange-500 flex-shrink-0" />}
|
||||
<span>{item.description}</span>
|
||||
{item.source_type === 'hometax' && (
|
||||
<span className="ml-1 text-xs text-orange-500">[홈택스]</span>
|
||||
)}
|
||||
{item.card_tx && (
|
||||
<span className={`ml-1 px-1.5 py-0.5 rounded text-[10px] font-medium flex-shrink-0 ${item.card_tx.deduction_type === 'non_deductible' ? 'bg-red-100 text-red-600' : 'bg-emerald-100 text-emerald-600'}`}>
|
||||
{item.card_tx.deduction_type === 'non_deductible' ? '불공제' : '공제'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.card_tx && (
|
||||
<div className="text-[11px] text-gray-400 mt-0.5">
|
||||
{item.card_tx.card_company_name} {'····' + item.card_tx.card_num.slice(-4)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className={cellBorder + ' px-3 py-1.5 text-gray-600'}>{item.trading_partner_name}</td>
|
||||
|
||||
@@ -1395,12 +1395,23 @@ className={`px-2.5 py-1 text-xs rounded-full font-medium transition-colors ${vie
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{row.type === 'card' && row.cardTx && (
|
||||
<div className="text-[11px] text-stone-400 mt-0.5 ml-[22px]">
|
||||
{row.cardTx.cardCompanyName} {'····' + row.cardTx.cardNum.slice(-4)}
|
||||
{row.cardTx.merchantBizNum && <span className="ml-2 font-mono">{row.cardTx.merchantBizNum}</span>}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-right font-medium text-blue-600 text-sm">
|
||||
{row.deposit > 0 ? formatCurrency(row.deposit) : ''}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-right font-medium text-red-600 text-sm">
|
||||
{row.withdraw > 0 ? formatCurrency(row.withdraw) : ''}
|
||||
{row.type === 'card' && row.cardTx && row.withdraw > 0 && (
|
||||
<div className="text-[10px] text-stone-400 font-normal mt-0.5">
|
||||
공급 {formatCurrency(row.cardTx.supplyAmount)} / 세액 {formatCurrency(row.cardTx.taxAmount)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-right text-stone-500 text-sm">
|
||||
{row.balance !== null ? formatCurrency(row.balance) : ''}
|
||||
|
||||
Reference in New Issue
Block a user