feat:입출금내역 계정과목 추가 및 엑셀 다운로드 기능
- BankTransaction 모델: 입출금 내역 저장 (계정과목 포함) - 바로빌 데이터와 DB 저장 데이터 매칭하여 계정과목 유지 - 계정과목 드롭다운 선택 및 저장 기능 - 엑셀(CSV) 다운로드 기능 - 저장된 행은 녹색 배경으로 표시 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,15 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Barobill\BarobillConfig;
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Models\Barobill\BankTransaction;
|
||||
use App\Models\Tenants\Tenant;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* 바로빌 계좌 입출금내역 조회 컨트롤러
|
||||
@@ -203,9 +206,12 @@ public function transactions(Request $request): JsonResponse
|
||||
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
||||
$userId = $barobillMember?->barobill_id ?? '';
|
||||
|
||||
// DB에서 저장된 계정과목 데이터 조회
|
||||
$savedData = BankTransaction::getByDateRange($tenantId, $startDate, $endDate, $bankAccountNum ?: null);
|
||||
|
||||
// 전체 계좌 조회: 빈 값이면 모든 계좌의 거래 내역 조회
|
||||
if (empty($bankAccountNum)) {
|
||||
return $this->getAllAccountsTransactions($userId, $startDate, $endDate, $page, $limit);
|
||||
return $this->getAllAccountsTransactions($userId, $startDate, $endDate, $page, $limit, $savedData);
|
||||
}
|
||||
|
||||
// 단일 계좌 조회
|
||||
@@ -252,8 +258,8 @@ public function transactions(Request $request): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
// 데이터 파싱
|
||||
$logs = $this->parseTransactionLogs($resultData);
|
||||
// 데이터 파싱 (저장된 계정과목 병합)
|
||||
$logs = $this->parseTransactionLogs($resultData, '', $savedData);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
@@ -280,7 +286,7 @@ public function transactions(Request $request): JsonResponse
|
||||
/**
|
||||
* 전체 계좌의 거래 내역 조회
|
||||
*/
|
||||
private function getAllAccountsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit): JsonResponse
|
||||
private function getAllAccountsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit, $savedData = null): JsonResponse
|
||||
{
|
||||
// 먼저 계좌 목록 조회
|
||||
$accountResult = $this->callSoap('GetBankAccountEx', ['AvailOnly' => 0]);
|
||||
@@ -326,7 +332,7 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s
|
||||
$errorCode = $this->checkErrorCode($accData);
|
||||
|
||||
if (!$errorCode || in_array($errorCode, [-25005, -25001])) {
|
||||
$parsed = $this->parseTransactionLogs($accData, $acc->BankName ?? '');
|
||||
$parsed = $this->parseTransactionLogs($accData, $acc->BankName ?? '', $savedData);
|
||||
foreach ($parsed['logs'] as $log) {
|
||||
$log['bankName'] = $acc->BankName ?? $this->getBankName($acc->BankCode ?? '');
|
||||
$allLogs[] = $log;
|
||||
@@ -370,9 +376,9 @@ private function getAllAccountsTransactions(string $userId, string $startDate, s
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래 내역 파싱
|
||||
* 거래 내역 파싱 (저장된 계정과목 병합)
|
||||
*/
|
||||
private function parseTransactionLogs($resultData, string $defaultBankName = ''): array
|
||||
private function parseTransactionLogs($resultData, string $defaultBankName = '', $savedData = null): array
|
||||
{
|
||||
$logs = [];
|
||||
$totalDeposit = 0;
|
||||
@@ -388,6 +394,7 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '')
|
||||
foreach ($rawLogs as $log) {
|
||||
$deposit = floatval($log->Deposit ?? 0);
|
||||
$withdraw = floatval($log->Withdraw ?? 0);
|
||||
$balance = floatval($log->Balance ?? 0);
|
||||
$totalDeposit += $deposit;
|
||||
$totalWithdraw += $withdraw;
|
||||
|
||||
@@ -413,23 +420,35 @@ private function parseTransactionLogs($resultData, string $defaultBankName = '')
|
||||
$fullSummary = $fullSummary ? $fullSummary . ' ' . $remark2 : $remark2;
|
||||
}
|
||||
|
||||
$logs[] = [
|
||||
$bankAccountNum = $log->BankAccountNum ?? '';
|
||||
|
||||
// 고유 키 생성하여 저장된 데이터와 매칭
|
||||
$uniqueKey = implode('|', [$bankAccountNum, $transDT, $deposit, $withdraw, $balance]);
|
||||
$savedItem = $savedData?->get($uniqueKey);
|
||||
|
||||
$logItem = [
|
||||
'transDate' => $transDate,
|
||||
'transTime' => $transTime,
|
||||
'transDateTime' => $dateTime,
|
||||
'bankAccountNum' => $log->BankAccountNum ?? '',
|
||||
'bankAccountNum' => $bankAccountNum,
|
||||
'bankName' => $log->BankName ?? $defaultBankName,
|
||||
'deposit' => $deposit,
|
||||
'withdraw' => $withdraw,
|
||||
'depositFormatted' => number_format($deposit),
|
||||
'withdrawFormatted' => number_format($withdraw),
|
||||
'balance' => floatval($log->Balance ?? 0),
|
||||
'balanceFormatted' => number_format(floatval($log->Balance ?? 0)),
|
||||
'balance' => $balance,
|
||||
'balanceFormatted' => number_format($balance),
|
||||
'summary' => $fullSummary,
|
||||
'cast' => $log->Cast ?? '',
|
||||
'memo' => $log->Memo ?? '',
|
||||
'transOffice' => $log->TransOffice ?? ''
|
||||
'transOffice' => $log->TransOffice ?? '',
|
||||
// 저장된 계정과목 정보 병합
|
||||
'accountCode' => $savedItem?->account_code ?? '',
|
||||
'accountName' => $savedItem?->account_name ?? '',
|
||||
'isSaved' => $savedItem !== null,
|
||||
];
|
||||
|
||||
$logs[] = $logItem;
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -507,6 +526,233 @@ private function getBankName(string $code): string
|
||||
return $banks[$code] ?? $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 계정과목 목록 조회
|
||||
*/
|
||||
public function accountCodes(): JsonResponse
|
||||
{
|
||||
// 자주 사용되는 계정과목 목록
|
||||
$codes = [
|
||||
['code' => '101', 'name' => '현금'],
|
||||
['code' => '103', 'name' => '보통예금'],
|
||||
['code' => '108', 'name' => '외상매출금'],
|
||||
['code' => '110', 'name' => '받을어음'],
|
||||
['code' => '253', 'name' => '외상매입금'],
|
||||
['code' => '255', 'name' => '지급어음'],
|
||||
['code' => '401', 'name' => '매출'],
|
||||
['code' => '501', 'name' => '매입'],
|
||||
['code' => '511', 'name' => '급여'],
|
||||
['code' => '521', 'name' => '복리후생비'],
|
||||
['code' => '522', 'name' => '여비교통비'],
|
||||
['code' => '523', 'name' => '접대비'],
|
||||
['code' => '524', 'name' => '통신비'],
|
||||
['code' => '525', 'name' => '수도광열비'],
|
||||
['code' => '526', 'name' => '세금과공과'],
|
||||
['code' => '527', 'name' => '임차료'],
|
||||
['code' => '528', 'name' => '수선비'],
|
||||
['code' => '529', 'name' => '보험료'],
|
||||
['code' => '530', 'name' => '차량유지비'],
|
||||
['code' => '531', 'name' => '운반비'],
|
||||
['code' => '532', 'name' => '교육훈련비'],
|
||||
['code' => '533', 'name' => '도서인쇄비'],
|
||||
['code' => '534', 'name' => '사무용품비'],
|
||||
['code' => '535', 'name' => '소모품비'],
|
||||
['code' => '536', 'name' => '지급수수료'],
|
||||
['code' => '537', 'name' => '광고선전비'],
|
||||
['code' => '538', 'name' => '대손상각비'],
|
||||
['code' => '539', 'name' => '감가상각비'],
|
||||
['code' => '540', 'name' => '잡비'],
|
||||
['code' => '901', 'name' => '이자수익'],
|
||||
['code' => '902', 'name' => '이자비용'],
|
||||
['code' => '910', 'name' => '잡이익'],
|
||||
['code' => '920', 'name' => '잡손실'],
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $codes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 입출금 내역 저장 (계정과목 포함)
|
||||
*/
|
||||
public function save(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
$transactions = $request->input('transactions', []);
|
||||
|
||||
if (empty($transactions)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '저장할 데이터가 없습니다.'
|
||||
]);
|
||||
}
|
||||
|
||||
$saved = 0;
|
||||
$updated = 0;
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
foreach ($transactions as $trans) {
|
||||
// 거래일시 생성
|
||||
$transDt = ($trans['transDate'] ?? '') . ($trans['transTime'] ?? '');
|
||||
|
||||
$data = [
|
||||
'tenant_id' => $tenantId,
|
||||
'bank_account_num' => $trans['bankAccountNum'] ?? '',
|
||||
'bank_code' => $trans['bankCode'] ?? '',
|
||||
'bank_name' => $trans['bankName'] ?? '',
|
||||
'trans_date' => $trans['transDate'] ?? '',
|
||||
'trans_time' => $trans['transTime'] ?? '',
|
||||
'trans_dt' => $transDt,
|
||||
'deposit' => floatval($trans['deposit'] ?? 0),
|
||||
'withdraw' => floatval($trans['withdraw'] ?? 0),
|
||||
'balance' => floatval($trans['balance'] ?? 0),
|
||||
'summary' => $trans['summary'] ?? '',
|
||||
'cast' => $trans['cast'] ?? '',
|
||||
'memo' => $trans['memo'] ?? '',
|
||||
'trans_office' => $trans['transOffice'] ?? '',
|
||||
'account_code' => $trans['accountCode'] ?? null,
|
||||
'account_name' => $trans['accountName'] ?? null,
|
||||
];
|
||||
|
||||
// Upsert: 있으면 업데이트, 없으면 생성
|
||||
$existing = BankTransaction::where('tenant_id', $tenantId)
|
||||
->where('bank_account_num', $data['bank_account_num'])
|
||||
->where('trans_dt', $transDt)
|
||||
->where('deposit', $data['deposit'])
|
||||
->where('withdraw', $data['withdraw'])
|
||||
->where('balance', $data['balance'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
// 계정과목만 업데이트
|
||||
$existing->update([
|
||||
'account_code' => $data['account_code'],
|
||||
'account_name' => $data['account_name'],
|
||||
]);
|
||||
$updated++;
|
||||
} else {
|
||||
BankTransaction::create($data);
|
||||
$saved++;
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "저장 완료: 신규 {$saved}건, 수정 {$updated}건",
|
||||
'saved' => $saved,
|
||||
'updated' => $updated
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('입출금 내역 저장 오류: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '저장 오류: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 엑셀 다운로드
|
||||
*/
|
||||
public function exportExcel(Request $request): StreamedResponse|JsonResponse
|
||||
{
|
||||
try {
|
||||
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
||||
$startDate = $request->input('startDate', date('Ymd'));
|
||||
$endDate = $request->input('endDate', date('Ymd'));
|
||||
$accountNum = $request->input('accountNum', '');
|
||||
|
||||
// DB에서 저장된 데이터 조회
|
||||
$query = BankTransaction::where('tenant_id', $tenantId)
|
||||
->whereBetween('trans_date', [$startDate, $endDate])
|
||||
->orderBy('trans_date', 'desc')
|
||||
->orderBy('trans_time', 'desc');
|
||||
|
||||
if (!empty($accountNum)) {
|
||||
$query->where('bank_account_num', $accountNum);
|
||||
}
|
||||
|
||||
$transactions = $query->get();
|
||||
|
||||
// 데이터가 없으면 바로빌에서 조회 (저장 안된 경우)
|
||||
if ($transactions->isEmpty()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '저장된 데이터가 없습니다. 먼저 데이터를 조회하고 저장해주세요.'
|
||||
]);
|
||||
}
|
||||
|
||||
$filename = "입출금내역_{$startDate}_{$endDate}.csv";
|
||||
|
||||
return response()->streamDownload(function () use ($transactions) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
|
||||
// UTF-8 BOM for Excel
|
||||
fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
// 헤더
|
||||
fputcsv($handle, [
|
||||
'거래일시',
|
||||
'은행명',
|
||||
'계좌번호',
|
||||
'적요',
|
||||
'입금',
|
||||
'출금',
|
||||
'잔액',
|
||||
'상대방',
|
||||
'계정과목코드',
|
||||
'계정과목명'
|
||||
]);
|
||||
|
||||
// 데이터
|
||||
foreach ($transactions as $trans) {
|
||||
$dateTime = '';
|
||||
if ($trans->trans_date) {
|
||||
$dateTime = substr($trans->trans_date, 0, 4) . '-' .
|
||||
substr($trans->trans_date, 4, 2) . '-' .
|
||||
substr($trans->trans_date, 6, 2);
|
||||
if ($trans->trans_time) {
|
||||
$dateTime .= ' ' . substr($trans->trans_time, 0, 2) . ':' .
|
||||
substr($trans->trans_time, 2, 2) . ':' .
|
||||
substr($trans->trans_time, 4, 2);
|
||||
}
|
||||
}
|
||||
|
||||
fputcsv($handle, [
|
||||
$dateTime,
|
||||
$trans->bank_name,
|
||||
$trans->bank_account_num,
|
||||
$trans->summary,
|
||||
$trans->deposit,
|
||||
$trans->withdraw,
|
||||
$trans->balance,
|
||||
$trans->cast,
|
||||
$trans->account_code,
|
||||
$trans->account_name
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=utf-8',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('엑셀 다운로드 오류: ' . $e->getMessage());
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => '다운로드 오류: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SOAP 호출
|
||||
*/
|
||||
|
||||
96
app/Models/Barobill/BankTransaction.php
Normal file
96
app/Models/Barobill/BankTransaction.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Barobill;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\Tenants\Tenant;
|
||||
|
||||
/**
|
||||
* 계좌 입출금 내역 모델
|
||||
*
|
||||
* 바로빌에서 조회한 입출금 내역에 계정과목을 추가하여 저장
|
||||
*/
|
||||
class BankTransaction extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'bank_account_num',
|
||||
'bank_code',
|
||||
'bank_name',
|
||||
'trans_date',
|
||||
'trans_time',
|
||||
'trans_dt',
|
||||
'deposit',
|
||||
'withdraw',
|
||||
'balance',
|
||||
'summary',
|
||||
'cast',
|
||||
'memo',
|
||||
'trans_office',
|
||||
'account_code',
|
||||
'account_name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'deposit' => 'decimal:2',
|
||||
'withdraw' => 'decimal:2',
|
||||
'balance' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* 테넌트 관계
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 거래 고유 키 생성 (매칭용)
|
||||
*/
|
||||
public function getUniqueKeyAttribute(): string
|
||||
{
|
||||
return implode('|', [
|
||||
$this->bank_account_num,
|
||||
$this->trans_dt,
|
||||
$this->deposit,
|
||||
$this->withdraw,
|
||||
$this->balance,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 로그 데이터로부터 고유 키 생성 (정적 메서드)
|
||||
*/
|
||||
public static function generateUniqueKey(array $log): string
|
||||
{
|
||||
// trans_dt 생성: transDate + transTime
|
||||
$transDt = ($log['transDate'] ?? '') . ($log['transTime'] ?? '');
|
||||
|
||||
return implode('|', [
|
||||
$log['bankAccountNum'] ?? '',
|
||||
$transDt,
|
||||
$log['deposit'] ?? 0,
|
||||
$log['withdraw'] ?? 0,
|
||||
$log['balance'] ?? 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 테넌트별 거래 내역 조회 (기간별)
|
||||
*/
|
||||
public static function getByDateRange(int $tenantId, string $startDate, string $endDate, ?string $accountNum = null)
|
||||
{
|
||||
$query = self::where('tenant_id', $tenantId)
|
||||
->whereBetween('trans_date', [$startDate, $endDate])
|
||||
->orderBy('trans_date', 'desc')
|
||||
->orderBy('trans_time', 'desc');
|
||||
|
||||
if ($accountNum) {
|
||||
$query->where('bank_account_num', $accountNum);
|
||||
}
|
||||
|
||||
return $query->get()->keyBy(fn($item) => $item->unique_key);
|
||||
}
|
||||
}
|
||||
@@ -63,12 +63,15 @@
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect, useRef } = React;
|
||||
const { useState, useEffect, useRef, useCallback } = React;
|
||||
|
||||
// API Routes
|
||||
const API = {
|
||||
accounts: '{{ route("barobill.eaccount.accounts") }}',
|
||||
transactions: '{{ route("barobill.eaccount.transactions") }}',
|
||||
accountCodes: '{{ route("barobill.eaccount.account-codes") }}',
|
||||
save: '{{ route("barobill.eaccount.save") }}',
|
||||
export: '{{ route("barobill.eaccount.export") }}',
|
||||
};
|
||||
|
||||
const CSRF_TOKEN = '{{ csrf_token() }}';
|
||||
@@ -86,6 +89,15 @@
|
||||
};
|
||||
};
|
||||
|
||||
// Toast 알림
|
||||
const showToast = (message, type = 'info') => {
|
||||
if (window.showToast) {
|
||||
window.showToast(message, type);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
};
|
||||
|
||||
// StatCard Component
|
||||
const StatCard = ({ title, value, subtext, icon, color = 'blue' }) => {
|
||||
const colorClasses = {
|
||||
@@ -138,8 +150,41 @@ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
</div>
|
||||
);
|
||||
|
||||
// AccountCodeSelect Component
|
||||
const AccountCodeSelect = ({ value, onChange, accountCodes }) => (
|
||||
<select
|
||||
value={value || ''}
|
||||
onChange={(e) => {
|
||||
const selected = accountCodes.find(c => c.code === e.target.value);
|
||||
onChange(e.target.value, selected?.name || '');
|
||||
}}
|
||||
className="w-full px-2 py-1 text-xs border border-stone-200 rounded focus:ring-2 focus:ring-emerald-500 outline-none bg-white"
|
||||
>
|
||||
<option value="">선택</option>
|
||||
{accountCodes.map(code => (
|
||||
<option key={code.code} value={code.code}>{code.code} {code.name}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
// TransactionTable Component
|
||||
const TransactionTable = ({ logs, loading, dateFrom, dateTo, onDateFromChange, onDateToChange, onThisMonth, onLastMonth, totalCount }) => {
|
||||
const TransactionTable = ({
|
||||
logs,
|
||||
loading,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
onDateFromChange,
|
||||
onDateToChange,
|
||||
onThisMonth,
|
||||
onLastMonth,
|
||||
totalCount,
|
||||
accountCodes,
|
||||
onAccountCodeChange,
|
||||
onSave,
|
||||
onExport,
|
||||
saving,
|
||||
hasChanges
|
||||
}) => {
|
||||
const formatCurrency = (val) => new Intl.NumberFormat('ko-KR').format(val || 0) + '원';
|
||||
|
||||
if (loading) {
|
||||
@@ -195,6 +240,40 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* 저장/엑셀 버튼 */}
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<button
|
||||
onClick={onSave}
|
||||
disabled={saving || logs.length === 0}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
hasChanges
|
||||
? 'bg-emerald-600 text-white hover:bg-emerald-700'
|
||||
: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{saving ? (
|
||||
<svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
||||
</svg>
|
||||
)}
|
||||
{hasChanges ? '변경사항 저장' : '저장'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onExport}
|
||||
disabled={logs.length === 0}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-100 text-blue-700 rounded-lg text-sm font-medium hover:bg-blue-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
엑셀 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto" style={ {maxHeight: '500px', overflowY: 'auto'} }>
|
||||
<table className="w-full text-left text-sm text-stone-600">
|
||||
@@ -207,18 +286,19 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
<th className="px-4 py-4 text-right bg-stone-50 text-red-600">출금</th>
|
||||
<th className="px-4 py-4 text-right bg-stone-50">잔액</th>
|
||||
<th className="px-4 py-4 bg-stone-50">상대방</th>
|
||||
<th className="px-4 py-4 bg-stone-50 min-w-[150px]">계정과목</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-stone-100">
|
||||
{logs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="7" className="px-6 py-8 text-center text-stone-400">
|
||||
<td colSpan="8" className="px-6 py-8 text-center text-stone-400">
|
||||
해당 기간에 조회된 입출금 내역이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<tr key={index} className="hover:bg-stone-50 transition-colors">
|
||||
<tr key={index} className={`hover:bg-stone-50 transition-colors ${log.isSaved ? 'bg-green-50/30' : ''}`}>
|
||||
<td className="px-4 py-3 whitespace-nowrap">
|
||||
<div className="font-medium text-stone-900">{log.transDateTime || '-'}</div>
|
||||
</td>
|
||||
@@ -244,6 +324,16 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
<td className="px-4 py-3 text-stone-500">
|
||||
{log.cast || '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<AccountCodeSelect
|
||||
value={log.accountCode}
|
||||
onChange={(code, name) => onAccountCodeChange(index, code, name)}
|
||||
accountCodes={accountCodes}
|
||||
/>
|
||||
{log.accountName && (
|
||||
<div className="text-xs text-emerald-600 mt-1">{log.accountName}</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
@@ -257,12 +347,15 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
// Main App Component
|
||||
const App = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [selectedAccount, setSelectedAccount] = useState('');
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [summary, setSummary] = useState({});
|
||||
const [pagination, setPagination] = useState({});
|
||||
const [error, setError] = useState(null);
|
||||
const [accountCodes, setAccountCodes] = useState([]);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
// 날짜 필터 상태 (기본: 현재 월)
|
||||
const currentMonth = getMonthDates(0);
|
||||
@@ -272,6 +365,7 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
// 초기 로드
|
||||
useEffect(() => {
|
||||
loadAccounts();
|
||||
loadAccountCodes();
|
||||
}, []);
|
||||
|
||||
// 날짜 또는 계좌 변경 시 거래내역 로드
|
||||
@@ -293,9 +387,22 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
}
|
||||
};
|
||||
|
||||
const loadAccountCodes = async () => {
|
||||
try {
|
||||
const response = await fetch(API.accountCodes);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setAccountCodes(data.data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('계정과목 목록 로드 오류:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTransactions = async (page = 1) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setHasChanges(false);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
@@ -325,6 +432,62 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
}
|
||||
};
|
||||
|
||||
// 계정과목 변경 핸들러
|
||||
const handleAccountCodeChange = useCallback((index, code, name) => {
|
||||
setLogs(prevLogs => {
|
||||
const newLogs = [...prevLogs];
|
||||
newLogs[index] = {
|
||||
...newLogs[index],
|
||||
accountCode: code,
|
||||
accountName: name
|
||||
};
|
||||
return newLogs;
|
||||
});
|
||||
setHasChanges(true);
|
||||
}, []);
|
||||
|
||||
// 저장 핸들러
|
||||
const handleSave = async () => {
|
||||
if (logs.length === 0) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await fetch(API.save, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': CSRF_TOKEN,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ transactions: logs })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
setHasChanges(false);
|
||||
// 저장 후 다시 로드하여 isSaved 상태 갱신
|
||||
loadTransactions();
|
||||
} else {
|
||||
showToast(data.error || '저장 실패', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('저장 오류: ' + err.message, 'error');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 엑셀 다운로드 핸들러
|
||||
const handleExport = () => {
|
||||
const params = new URLSearchParams({
|
||||
startDate: dateFrom.replace(/-/g, ''),
|
||||
endDate: dateTo.replace(/-/g, ''),
|
||||
accountNum: selectedAccount
|
||||
});
|
||||
window.location.href = `${API.export}?${params}`;
|
||||
};
|
||||
|
||||
// 이번 달 버튼
|
||||
const handleThisMonth = () => {
|
||||
const dates = getMonthDates(0);
|
||||
@@ -347,7 +510,7 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-stone-900">계좌 입출금내역</h1>
|
||||
<p className="text-stone-500 mt-1">바로빌 API를 통한 계좌 입출금내역 조회</p>
|
||||
<p className="text-stone-500 mt-1">바로빌 API를 통한 계좌 입출금내역 조회 및 계정과목 관리</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@if($isTestMode)
|
||||
@@ -441,6 +604,12 @@ className="px-3 py-1.5 text-sm bg-stone-100 text-stone-600 rounded-lg hover:bg-s
|
||||
onThisMonth={handleThisMonth}
|
||||
onLastMonth={handleLastMonth}
|
||||
totalCount={summary.count || logs.length}
|
||||
accountCodes={accountCodes}
|
||||
onAccountCodeChange={handleAccountCodeChange}
|
||||
onSave={handleSave}
|
||||
onExport={handleExport}
|
||||
saving={saving}
|
||||
hasChanges={hasChanges}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -292,6 +292,9 @@
|
||||
Route::get('/', [\App\Http\Controllers\Barobill\EaccountController::class, 'index'])->name('index');
|
||||
Route::get('/accounts', [\App\Http\Controllers\Barobill\EaccountController::class, 'accounts'])->name('accounts');
|
||||
Route::get('/transactions', [\App\Http\Controllers\Barobill\EaccountController::class, 'transactions'])->name('transactions');
|
||||
Route::get('/account-codes', [\App\Http\Controllers\Barobill\EaccountController::class, 'accountCodes'])->name('account-codes');
|
||||
Route::post('/save', [\App\Http\Controllers\Barobill\EaccountController::class, 'save'])->name('save');
|
||||
Route::get('/export', [\App\Http\Controllers\Barobill\EaccountController::class, 'exportExcel'])->name('export');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user