인라인 편집으로 cast 저장 시 override 테이블의 modified_cast가 메인 테이블 값보다 우선하여 이전 값이 표시되는 문제. save() 후 override의 modified_cast를 동기화(제거)하여 충돌 방지. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1655 lines
62 KiB
PHP
1655 lines
62 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Barobill;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Barobill\AccountCode;
|
|
use App\Models\Barobill\BarobillConfig;
|
|
use App\Models\Barobill\BarobillMember;
|
|
use App\Models\Barobill\BankTransaction;
|
|
use App\Models\Barobill\BankTransactionOverride;
|
|
use App\Models\Barobill\BankTransactionSplit;
|
|
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;
|
|
|
|
/**
|
|
* 바로빌 계좌 입출금내역 조회 컨트롤러
|
|
*/
|
|
class EaccountController extends Controller
|
|
{
|
|
/**
|
|
* 바로빌 SOAP 설정
|
|
*/
|
|
private ?string $certKey = null;
|
|
private ?string $corpNum = null;
|
|
private bool $isTestMode = false;
|
|
private ?string $soapUrl = null;
|
|
private ?\SoapClient $soapClient = null;
|
|
|
|
// 바로빌 파트너사 (본사) 테넌트 ID
|
|
private const HEADQUARTERS_TENANT_ID = 1;
|
|
|
|
public function __construct()
|
|
{
|
|
// DB에서 활성화된 바로빌 설정 조회
|
|
$activeConfig = BarobillConfig::where('is_active', true)->first();
|
|
|
|
if ($activeConfig) {
|
|
$this->certKey = $activeConfig->cert_key;
|
|
$this->corpNum = $activeConfig->corp_num;
|
|
$this->isTestMode = $activeConfig->environment === 'test';
|
|
// 계좌 조회는 BANKACCOUNT.asmx 사용
|
|
$baseUrl = $this->isTestMode
|
|
? 'https://testws.baroservice.com'
|
|
: 'https://ws.baroservice.com';
|
|
$this->soapUrl = $baseUrl . '/BANKACCOUNT.asmx?WSDL';
|
|
} else {
|
|
$this->isTestMode = config('services.barobill.test_mode', true);
|
|
// 테스트 모드에 따라 적절한 CERT_KEY 선택
|
|
$this->certKey = $this->isTestMode
|
|
? config('services.barobill.cert_key_test', '')
|
|
: config('services.barobill.cert_key_prod', '');
|
|
$this->corpNum = config('services.barobill.corp_num', '');
|
|
$this->soapUrl = $this->isTestMode
|
|
? 'https://testws.baroservice.com/BANKACCOUNT.asmx?WSDL'
|
|
: 'https://ws.baroservice.com/BANKACCOUNT.asmx?WSDL';
|
|
}
|
|
|
|
$this->initSoapClient();
|
|
}
|
|
|
|
/**
|
|
* SOAP 클라이언트 초기화
|
|
*/
|
|
private function initSoapClient(): void
|
|
{
|
|
if (!empty($this->certKey) || $this->isTestMode) {
|
|
try {
|
|
$context = stream_context_create([
|
|
'ssl' => [
|
|
'verify_peer' => false,
|
|
'verify_peer_name' => false,
|
|
'allow_self_signed' => true
|
|
]
|
|
]);
|
|
|
|
$this->soapClient = new \SoapClient($this->soapUrl, [
|
|
'trace' => true,
|
|
'encoding' => 'UTF-8',
|
|
'exceptions' => true,
|
|
'connection_timeout' => 30,
|
|
'stream_context' => $context,
|
|
'cache_wsdl' => WSDL_CACHE_NONE
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('바로빌 계좌 SOAP 클라이언트 생성 실패: ' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계좌 입출금내역 메인 페이지
|
|
*/
|
|
public function index(Request $request): View|Response
|
|
{
|
|
if ($request->header('HX-Request')) {
|
|
return response('', 200)->header('HX-Redirect', route('finance.account-transactions'));
|
|
}
|
|
|
|
// 현재 선택된 테넌트 정보
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$currentTenant = Tenant::find($tenantId);
|
|
|
|
// 해당 테넌트의 바로빌 회원사 정보
|
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
|
|
// 테넌트별 서버 모드 적용 (회원사 설정 우선)
|
|
$isTestMode = $barobillMember ? $barobillMember->isTestMode() : $this->isTestMode;
|
|
|
|
// 서버 모드에 따라 SOAP 설정 재초기화
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
return view('barobill.eaccount.index', [
|
|
'certKey' => $this->certKey,
|
|
'corpNum' => $this->corpNum,
|
|
'isTestMode' => $isTestMode,
|
|
'hasSoapClient' => $this->soapClient !== null,
|
|
'currentTenant' => $currentTenant,
|
|
'barobillMember' => $barobillMember,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 회원사 서버 모드에 따라 SOAP 설정 적용
|
|
*/
|
|
private function applyMemberServerMode(BarobillMember $member): void
|
|
{
|
|
$memberTestMode = $member->isTestMode();
|
|
$targetEnv = $memberTestMode ? 'test' : 'production';
|
|
|
|
// 해당 환경의 BarobillConfig 조회 (is_active 무관하게 환경에 맞는 설정 사용)
|
|
$config = BarobillConfig::where('environment', $targetEnv)->first();
|
|
|
|
if ($config) {
|
|
$this->isTestMode = $memberTestMode;
|
|
$this->certKey = $config->cert_key;
|
|
$this->corpNum = $config->corp_num;
|
|
$baseUrl = $config->base_url ?: ($memberTestMode
|
|
? 'https://testws.baroservice.com'
|
|
: 'https://ws.baroservice.com');
|
|
$this->soapUrl = $baseUrl . '/BANKACCOUNT.asmx?WSDL';
|
|
|
|
// SOAP 클라이언트 재초기화
|
|
$this->initSoapClient();
|
|
|
|
Log::info('[Eaccount] 서버 모드 적용', [
|
|
'targetEnv' => $targetEnv,
|
|
'certKey' => substr($this->certKey ?? '', 0, 10) . '...',
|
|
'corpNum' => $this->corpNum,
|
|
'soapUrl' => $this->soapUrl,
|
|
]);
|
|
} else {
|
|
Log::warning('[Eaccount] BarobillConfig 없음', ['targetEnv' => $targetEnv]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 등록된 계좌 목록 조회 (GetBankAccountEx)
|
|
*/
|
|
public function accounts(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$availOnly = $request->input('availOnly', 0);
|
|
|
|
// 현재 테넌트의 바로빌 회원 정보 조회
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
|
|
// 테넌트별 서버 모드 적용
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
// 바로빌 사용자 ID 결정
|
|
$userId = $barobillMember?->barobill_id ?? '';
|
|
|
|
$result = $this->callSoap('GetBankAccountEx', [
|
|
'AvailOnly' => (int)$availOnly
|
|
]);
|
|
|
|
if (!$result['success']) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $result['error'],
|
|
'error_code' => $result['error_code'] ?? null
|
|
]);
|
|
}
|
|
|
|
$accounts = [];
|
|
$data = $result['data'];
|
|
|
|
// BankAccount 또는 BankAccountEx에서 계좌 목록 추출
|
|
$accountList = [];
|
|
if (isset($data->BankAccount)) {
|
|
$accountList = is_array($data->BankAccount) ? $data->BankAccount : [$data->BankAccount];
|
|
} elseif (isset($data->BankAccountEx)) {
|
|
$accountList = is_array($data->BankAccountEx) ? $data->BankAccountEx : [$data->BankAccountEx];
|
|
}
|
|
|
|
foreach ($accountList as $acc) {
|
|
if (!is_object($acc)) continue;
|
|
|
|
$bankAccountNum = $acc->BankAccountNum ?? '';
|
|
if (empty($bankAccountNum) || (is_numeric($bankAccountNum) && $bankAccountNum < 0)) {
|
|
continue;
|
|
}
|
|
|
|
$bankCode = $acc->BankCode ?? '';
|
|
$bankName = $acc->BankName ?? $this->getBankName($bankCode);
|
|
|
|
$accounts[] = [
|
|
'bankAccountNum' => $bankAccountNum,
|
|
'bankCode' => $bankCode,
|
|
'bankName' => $bankName,
|
|
'accountName' => $acc->AccountName ?? '',
|
|
'accountType' => $acc->AccountType ?? '',
|
|
'currency' => $acc->Currency ?? 'KRW',
|
|
'issueDate' => $acc->IssueDate ?? '',
|
|
'balance' => $acc->Balance ?? 0,
|
|
'status' => isset($acc->UseState) ? (int)$acc->UseState : 1
|
|
];
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'accounts' => $accounts,
|
|
'count' => count($accounts)
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('계좌 목록 조회 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '서버 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계좌별 최신 잔액 조회 (DB에서 조회 - 대시보드용)
|
|
*/
|
|
public function latestBalances(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
|
|
// 전체 거래를 한번에 조회 (계좌별 → 시간순)
|
|
$allTransactions = BankTransaction::where('tenant_id', $tenantId)
|
|
->select('bank_account_num', 'bank_name', 'balance', 'deposit', 'withdraw', 'trans_date', 'trans_time', 'is_manual')
|
|
->orderBy('bank_account_num')
|
|
->orderBy('trans_date', 'asc')
|
|
->orderBy('trans_time', 'asc')
|
|
->orderBy('id', 'asc')
|
|
->get();
|
|
|
|
// 계좌별로 그룹화하여 최종 잔액 계산
|
|
$accountBalances = [];
|
|
foreach ($allTransactions as $tx) {
|
|
$accNum = $tx->bank_account_num;
|
|
|
|
if (!isset($accountBalances[$accNum])) {
|
|
$accountBalances[$accNum] = [
|
|
'bankAccountNum' => $accNum,
|
|
'bankName' => $tx->bank_name,
|
|
'balance' => 0,
|
|
'lastTransDate' => '',
|
|
'lastTransTime' => '',
|
|
];
|
|
}
|
|
|
|
$prev = $accountBalances[$accNum]['balance'];
|
|
if (!$tx->is_manual && (float) $tx->balance != 0) {
|
|
$accountBalances[$accNum]['balance'] = (float) $tx->balance;
|
|
} else {
|
|
$accountBalances[$accNum]['balance'] = $prev + (float) $tx->deposit - (float) $tx->withdraw;
|
|
}
|
|
$accountBalances[$accNum]['lastTransDate'] = $tx->trans_date;
|
|
$accountBalances[$accNum]['lastTransTime'] = $tx->trans_time;
|
|
}
|
|
|
|
$result = array_values($accountBalances);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'balances' => $result,
|
|
'count' => count($result)
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('최신 잔액 조회 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계좌 입출금내역 조회 (GetPeriodBankAccountTransLog)
|
|
*/
|
|
public function transactions(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$startDate = $request->input('startDate', date('Ymd'));
|
|
$endDate = $request->input('endDate', date('Ymd'));
|
|
$bankAccountNum = str_replace('-', '', $request->input('accountNum', ''));
|
|
$page = (int)$request->input('page', 1);
|
|
$limit = (int)$request->input('limit', 50);
|
|
|
|
// 현재 테넌트의 바로빌 회원 정보 조회
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
$userId = $barobillMember?->barobill_id ?? '';
|
|
|
|
// 테넌트별 서버 모드 적용
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
// DB에서 저장된 계정과목 데이터 조회
|
|
$savedData = BankTransaction::getByDateRange($tenantId, $startDate, $endDate, $bankAccountNum ?: null);
|
|
|
|
// 오버라이드 데이터 (수정된 적요/내용) 조회
|
|
$overrideData = null;
|
|
|
|
// DB에서 수동 입력 건 조회
|
|
$manualQuery = BankTransaction::where('tenant_id', $tenantId)
|
|
->where('is_manual', true)
|
|
->whereBetween('trans_date', [$startDate, $endDate]);
|
|
if (!empty($bankAccountNum)) {
|
|
$manualQuery->where('bank_account_num', $bankAccountNum);
|
|
}
|
|
$manualTransactions = $manualQuery->orderBy('trans_date', 'desc')
|
|
->orderBy('trans_time', 'desc')
|
|
->get();
|
|
|
|
// 전체 계좌 조회: 빈 값이면 모든 계좌의 거래 내역 조회
|
|
if (empty($bankAccountNum)) {
|
|
return $this->getAllAccountsTransactions($userId, $startDate, $endDate, $page, $limit, $savedData, $overrideData, $tenantId, $manualTransactions);
|
|
}
|
|
|
|
// 단일 계좌 조회
|
|
$result = $this->callSoap('GetPeriodBankAccountTransLog', [
|
|
'ID' => $userId,
|
|
'BankAccountNum' => $bankAccountNum,
|
|
'StartDate' => $startDate,
|
|
'EndDate' => $endDate,
|
|
'TransDirection' => 1, // 1:전체
|
|
'CountPerPage' => $limit,
|
|
'CurrentPage' => $page,
|
|
'OrderDirection' => 2 // 2:내림차순
|
|
]);
|
|
|
|
if (!$result['success']) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $result['error'],
|
|
'error_code' => $result['error_code'] ?? null
|
|
]);
|
|
}
|
|
|
|
$resultData = $result['data'];
|
|
|
|
// 에러 코드 체크
|
|
$errorCode = $this->checkErrorCode($resultData);
|
|
if ($errorCode && !in_array($errorCode, [-25005, -25001])) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $this->getErrorMessage($errorCode),
|
|
'error_code' => $errorCode
|
|
]);
|
|
}
|
|
|
|
// 데이터가 없는 경우
|
|
if ($errorCode && in_array($errorCode, [-25005, -25001])) {
|
|
// API 데이터 없어도 수동 건은 표시
|
|
$manualLogs = $this->convertManualToLogs($manualTransactions);
|
|
$baseBalance = $this->findBaseBalance($tenantId, $startDate, $bankAccountNum);
|
|
$recalcLogs = $this->recalcManualBalances($manualLogs['logs'], $baseBalance);
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'logs' => $recalcLogs,
|
|
'summary' => $manualLogs['summary'],
|
|
'pagination' => ['currentPage' => 1, 'maxPageNum' => 1]
|
|
]
|
|
]);
|
|
}
|
|
|
|
// 데이터 파싱 (저장된 계정과목 + 오버라이드 병합)
|
|
$logs = $this->parseTransactionLogs($resultData, '', $savedData, $tenantId);
|
|
|
|
// 수동 입력 건 병합
|
|
$manualLogs = $this->convertManualToLogs($manualTransactions);
|
|
$mergedLogs = array_merge($logs['logs'], $manualLogs['logs']);
|
|
|
|
// 날짜/시간 기준 정렬 (최신순)
|
|
usort($mergedLogs, function ($a, $b) {
|
|
$dtA = ($a['transDate'] ?? '') . ($a['transTime'] ?? '');
|
|
$dtB = ($b['transDate'] ?? '') . ($b['transTime'] ?? '');
|
|
return strcmp($dtB, $dtA);
|
|
});
|
|
|
|
// 수동입력 건 잔액 재계산 (조회기간 이전 잔액 기준)
|
|
$baseBalance = $this->findBaseBalance($tenantId, $startDate, $bankAccountNum);
|
|
$mergedLogs = $this->recalcManualBalances($mergedLogs, $baseBalance);
|
|
|
|
// summary 합산
|
|
$mergedSummary = [
|
|
'totalDeposit' => $logs['summary']['totalDeposit'] + $manualLogs['summary']['totalDeposit'],
|
|
'totalWithdraw' => $logs['summary']['totalWithdraw'] + $manualLogs['summary']['totalWithdraw'],
|
|
'count' => $logs['summary']['count'] + $manualLogs['summary']['count'],
|
|
];
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'logs' => $mergedLogs,
|
|
'pagination' => [
|
|
'currentPage' => $resultData->CurrentPage ?? 1,
|
|
'countPerPage' => $resultData->CountPerPage ?? 50,
|
|
'maxPageNum' => $resultData->MaxPageNum ?? 1,
|
|
'maxIndex' => $resultData->MaxIndex ?? 0
|
|
],
|
|
'summary' => $mergedSummary
|
|
]
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('입출금내역 조회 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '서버 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전체 계좌의 거래 내역 조회
|
|
*/
|
|
private function getAllAccountsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit, $savedData = null, $overrideData = null, int $tenantId = 1, $manualTransactions = null): JsonResponse
|
|
{
|
|
// 먼저 계좌 목록 조회
|
|
$accountResult = $this->callSoap('GetBankAccountEx', ['AvailOnly' => 0]);
|
|
|
|
if (!$accountResult['success']) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $accountResult['error']
|
|
]);
|
|
}
|
|
|
|
$accountList = [];
|
|
$data = $accountResult['data'];
|
|
if (isset($data->BankAccount)) {
|
|
$accountList = is_array($data->BankAccount) ? $data->BankAccount : [$data->BankAccount];
|
|
} elseif (isset($data->BankAccountEx)) {
|
|
$accountList = is_array($data->BankAccountEx) ? $data->BankAccountEx : [$data->BankAccountEx];
|
|
}
|
|
|
|
$allLogs = [];
|
|
$totalDeposit = 0;
|
|
$totalWithdraw = 0;
|
|
|
|
foreach ($accountList as $acc) {
|
|
if (!is_object($acc)) continue;
|
|
|
|
$accNum = $acc->BankAccountNum ?? '';
|
|
if (empty($accNum) || (is_numeric($accNum) && $accNum < 0)) continue;
|
|
|
|
$accResult = $this->callSoap('GetPeriodBankAccountTransLog', [
|
|
'ID' => $userId,
|
|
'BankAccountNum' => $accNum,
|
|
'StartDate' => $startDate,
|
|
'EndDate' => $endDate,
|
|
'TransDirection' => 1,
|
|
'CountPerPage' => 1000,
|
|
'CurrentPage' => 1,
|
|
'OrderDirection' => 2
|
|
]);
|
|
|
|
if ($accResult['success']) {
|
|
$accData = $accResult['data'];
|
|
$errorCode = $this->checkErrorCode($accData);
|
|
|
|
if (!$errorCode || in_array($errorCode, [-25005, -25001])) {
|
|
$parsed = $this->parseTransactionLogs($accData, $acc->BankName ?? '', $savedData, $tenantId);
|
|
foreach ($parsed['logs'] as $log) {
|
|
$log['bankName'] = $acc->BankName ?? $this->getBankName($acc->BankCode ?? '');
|
|
$allLogs[] = $log;
|
|
}
|
|
$totalDeposit += $parsed['summary']['totalDeposit'];
|
|
$totalWithdraw += $parsed['summary']['totalWithdraw'];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 수동 입력 건 병합
|
|
if ($manualTransactions && $manualTransactions->isNotEmpty()) {
|
|
$manualLogs = $this->convertManualToLogs($manualTransactions);
|
|
foreach ($manualLogs['logs'] as $mLog) {
|
|
$allLogs[] = $mLog;
|
|
}
|
|
$totalDeposit += $manualLogs['summary']['totalDeposit'];
|
|
$totalWithdraw += $manualLogs['summary']['totalWithdraw'];
|
|
}
|
|
|
|
// 날짜/시간 기준 정렬 (최신순)
|
|
usort($allLogs, function ($a, $b) {
|
|
$dateA = ($a['transDate'] ?? '') . ($a['transTime'] ?? '');
|
|
$dateB = ($b['transDate'] ?? '') . ($b['transTime'] ?? '');
|
|
return strcmp($dateB, $dateA);
|
|
});
|
|
|
|
// 수동입력 건 잔액 재계산 (조회기간 이전 잔액 기준)
|
|
$baseBalance = $this->findBaseBalance($tenantId, $startDate);
|
|
$allLogs = $this->recalcManualBalances($allLogs, $baseBalance);
|
|
|
|
// 페이지네이션
|
|
$totalCount = count($allLogs);
|
|
$maxPageNum = (int)ceil($totalCount / $limit);
|
|
$startIndex = ($page - 1) * $limit;
|
|
$paginatedLogs = array_slice($allLogs, $startIndex, $limit);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'logs' => $paginatedLogs,
|
|
'pagination' => [
|
|
'currentPage' => $page,
|
|
'countPerPage' => $limit,
|
|
'maxPageNum' => $maxPageNum,
|
|
'maxIndex' => $totalCount
|
|
],
|
|
'summary' => [
|
|
'totalDeposit' => $totalDeposit,
|
|
'totalWithdraw' => $totalWithdraw,
|
|
'count' => $totalCount
|
|
]
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 거래 내역 파싱 (저장된 계정과목 + 오버라이드 병합)
|
|
*/
|
|
private function parseTransactionLogs($resultData, string $defaultBankName = '', $savedData = null, int $tenantId = 1): array
|
|
{
|
|
$logs = [];
|
|
$uniqueKeys = [];
|
|
$totalDeposit = 0;
|
|
$totalWithdraw = 0;
|
|
|
|
$rawLogs = [];
|
|
if (isset($resultData->BankAccountLogList) && isset($resultData->BankAccountLogList->BankAccountTransLog)) {
|
|
$rawLogs = is_array($resultData->BankAccountLogList->BankAccountTransLog)
|
|
? $resultData->BankAccountLogList->BankAccountTransLog
|
|
: [$resultData->BankAccountLogList->BankAccountTransLog];
|
|
}
|
|
|
|
// 1단계: 모든 고유 키 수집
|
|
foreach ($rawLogs as $log) {
|
|
$bankAccountNum = $log->BankAccountNum ?? '';
|
|
$transDT = $log->TransDT ?? '';
|
|
$deposit = (int) floatval($log->Deposit ?? 0);
|
|
$withdraw = (int) floatval($log->Withdraw ?? 0);
|
|
$balance = (int) floatval($log->Balance ?? 0);
|
|
|
|
$uniqueKey = implode('|', [$bankAccountNum, $transDT, $deposit, $withdraw, $balance]);
|
|
$uniqueKeys[] = $uniqueKey;
|
|
}
|
|
|
|
// 2단계: 오버라이드 데이터 일괄 조회
|
|
$overrides = BankTransactionOverride::getByUniqueKeys($tenantId, $uniqueKeys);
|
|
|
|
// 3단계: 각 로그 처리
|
|
foreach ($rawLogs as $log) {
|
|
$deposit = floatval($log->Deposit ?? 0);
|
|
$withdraw = floatval($log->Withdraw ?? 0);
|
|
$balance = floatval($log->Balance ?? 0);
|
|
$totalDeposit += $deposit;
|
|
$totalWithdraw += $withdraw;
|
|
|
|
// 거래일시 파싱
|
|
$transDT = $log->TransDT ?? '';
|
|
$transDate = '';
|
|
$transTime = '';
|
|
$dateTime = '';
|
|
|
|
if (!empty($transDT) && strlen($transDT) >= 14) {
|
|
$transDate = substr($transDT, 0, 8);
|
|
$transTime = substr($transDT, 8, 6);
|
|
$dateTime = substr($transDT, 0, 4) . '-' . substr($transDT, 4, 2) . '-' . substr($transDT, 6, 2) . ' ' .
|
|
substr($transDT, 8, 2) . ':' . substr($transDT, 10, 2) . ':' . substr($transDT, 12, 2);
|
|
}
|
|
|
|
// 적요 파싱
|
|
$summary = $log->TransRemark1 ?? $log->Summary ?? '';
|
|
$remark2 = $log->TransRemark2 ?? '';
|
|
$transType = $log->TransType ?? '';
|
|
$fullSummary = $summary;
|
|
if (!empty($remark2)) {
|
|
$fullSummary = $fullSummary ? $fullSummary . ' ' . $remark2 : $remark2;
|
|
}
|
|
|
|
$bankAccountNum = $log->BankAccountNum ?? '';
|
|
|
|
// 고유 키 생성하여 저장된 데이터와 매칭 (숫자는 정수로 변환하여 형식 통일)
|
|
$uniqueKey = implode('|', [$bankAccountNum, $transDT, (int) $deposit, (int) $withdraw, (int) $balance]);
|
|
$savedItem = $savedData?->get($uniqueKey);
|
|
$override = $overrides->get($uniqueKey);
|
|
|
|
// 원본 적요/내용
|
|
$originalSummary = $fullSummary;
|
|
$originalCast = $savedItem?->cast ?? '';
|
|
|
|
// 오버라이드 적용 (수정된 값이 있으면 사용)
|
|
$displaySummary = $override?->modified_summary ?? $originalSummary;
|
|
$displayCast = $override?->modified_cast ?? $originalCast;
|
|
|
|
$logItem = [
|
|
'transDate' => $transDate,
|
|
'transTime' => $transTime,
|
|
'transDateTime' => $dateTime,
|
|
'bankAccountNum' => $bankAccountNum,
|
|
'bankName' => $log->BankName ?? $defaultBankName,
|
|
'deposit' => $deposit,
|
|
'withdraw' => $withdraw,
|
|
'depositFormatted' => number_format($deposit),
|
|
'withdrawFormatted' => number_format($withdraw),
|
|
'balance' => $balance,
|
|
'balanceFormatted' => number_format($balance),
|
|
'summary' => $displaySummary,
|
|
'originalSummary' => $originalSummary,
|
|
'cast' => $displayCast,
|
|
'originalCast' => $originalCast,
|
|
'memo' => $log->Memo ?? '',
|
|
'transOffice' => $log->TransOffice ?? '',
|
|
// 저장된 계정과목 정보 병합
|
|
'accountCode' => $savedItem?->account_code ?? '',
|
|
'accountName' => $savedItem?->account_name ?? '',
|
|
'isSaved' => $savedItem !== null,
|
|
'isOverridden' => $override !== null,
|
|
'uniqueKey' => $uniqueKey,
|
|
];
|
|
|
|
$logs[] = $logItem;
|
|
}
|
|
|
|
return [
|
|
'logs' => $logs,
|
|
'summary' => [
|
|
'totalDeposit' => $totalDeposit,
|
|
'totalWithdraw' => $totalWithdraw,
|
|
'count' => count($logs)
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 에러 코드 체크
|
|
*/
|
|
private function checkErrorCode($data): ?int
|
|
{
|
|
if (isset($data->CurrentPage) && is_numeric($data->CurrentPage) && $data->CurrentPage < 0) {
|
|
return (int)$data->CurrentPage;
|
|
}
|
|
if (isset($data->BankAccountNum) && is_numeric($data->BankAccountNum) && $data->BankAccountNum < 0) {
|
|
return (int)$data->BankAccountNum;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 에러 메시지 반환
|
|
*/
|
|
private function getErrorMessage(int $errorCode): string
|
|
{
|
|
$messages = [
|
|
-10002 => '인증 실패 (-10002). CERTKEY가 올바르지 않거나 만료되었습니다.',
|
|
-50214 => '은행 로그인 실패 (-50214). 바로빌 사이트에서 계좌 비밀번호/인증서를 점검해주세요.',
|
|
-24005 => '사용자 정보 불일치 (-24005). 사업자번호를 확인해주세요.',
|
|
-25001 => '등록된 계좌가 없습니다 (-25001).',
|
|
-25005 => '조회된 데이터가 없습니다 (-25005).',
|
|
-25006 => '계좌번호가 잘못되었습니다 (-25006).',
|
|
-25007 => '조회 기간이 잘못되었습니다 (-25007).',
|
|
];
|
|
return $messages[$errorCode] ?? '바로빌 API 오류: ' . $errorCode;
|
|
}
|
|
|
|
/**
|
|
* 은행 코드 -> 은행명 변환
|
|
*/
|
|
private function getBankName(string $code): string
|
|
{
|
|
$banks = [
|
|
'002' => 'KDB산업은행',
|
|
'003' => 'IBK기업은행',
|
|
'004' => 'KB국민은행',
|
|
'007' => '수협은행',
|
|
'011' => 'NH농협은행',
|
|
'020' => '우리은행',
|
|
'023' => 'SC제일은행',
|
|
'027' => '한국씨티은행',
|
|
'031' => '대구은행',
|
|
'032' => '부산은행',
|
|
'034' => '광주은행',
|
|
'035' => '제주은행',
|
|
'037' => '전북은행',
|
|
'039' => '경남은행',
|
|
'045' => '새마을금고',
|
|
'048' => '신협',
|
|
'050' => '저축은행',
|
|
'064' => '산림조합',
|
|
'071' => '우체국',
|
|
'081' => '하나은행',
|
|
'088' => '신한은행',
|
|
'089' => 'K뱅크',
|
|
'090' => '카카오뱅크',
|
|
'092' => '토스뱅크'
|
|
];
|
|
return $banks[$code] ?? $code;
|
|
}
|
|
|
|
/**
|
|
* 계정과목 목록 조회 (글로벌 데이터)
|
|
*/
|
|
public function accountCodes(): JsonResponse
|
|
{
|
|
$codes = AccountCode::getActive();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $codes->map(fn($c) => [
|
|
'id' => $c->id,
|
|
'code' => $c->code,
|
|
'name' => $c->name,
|
|
'category' => $c->category,
|
|
])
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 전체 계정과목 목록 조회 (설정용, 비활성 포함, 글로벌 데이터)
|
|
*/
|
|
public function accountCodesAll(): JsonResponse
|
|
{
|
|
$codes = AccountCode::getAll();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $codes
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 계정과목 추가 (글로벌 데이터)
|
|
*/
|
|
public function accountCodeStore(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$validated = $request->validate([
|
|
'code' => 'required|string|max:10',
|
|
'name' => 'required|string|max:100',
|
|
'category' => 'nullable|string|max:50',
|
|
]);
|
|
|
|
// 중복 체크 (글로벌)
|
|
$exists = AccountCode::where('code', $validated['code'])->exists();
|
|
|
|
if ($exists) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '이미 존재하는 계정과목 코드입니다.'
|
|
], 422);
|
|
}
|
|
|
|
$maxSort = AccountCode::max('sort_order') ?? 0;
|
|
|
|
$accountCode = AccountCode::create([
|
|
'tenant_id' => self::HEADQUARTERS_TENANT_ID, // 글로벌 데이터는 기본 테넌트에 저장
|
|
'code' => $validated['code'],
|
|
'name' => $validated['name'],
|
|
'category' => $validated['category'] ?? null,
|
|
'sort_order' => $maxSort + 1,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '계정과목이 추가되었습니다.',
|
|
'data' => $accountCode
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '추가 실패: ' . $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계정과목 수정 (글로벌 데이터)
|
|
*/
|
|
public function accountCodeUpdate(Request $request, int $id): JsonResponse
|
|
{
|
|
try {
|
|
$accountCode = AccountCode::find($id);
|
|
|
|
if (!$accountCode) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '계정과목을 찾을 수 없습니다.'
|
|
], 404);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'code' => 'sometimes|string|max:10',
|
|
'name' => 'sometimes|string|max:100',
|
|
'category' => 'nullable|string|max:50',
|
|
'is_active' => 'sometimes|boolean',
|
|
]);
|
|
|
|
// 코드 변경 시 중복 체크 (글로벌)
|
|
if (isset($validated['code']) && $validated['code'] !== $accountCode->code) {
|
|
$exists = AccountCode::where('code', $validated['code'])
|
|
->where('id', '!=', $id)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '이미 존재하는 계정과목 코드입니다.'
|
|
], 422);
|
|
}
|
|
}
|
|
|
|
$accountCode->update($validated);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '계정과목이 수정되었습니다.',
|
|
'data' => $accountCode
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '수정 실패: ' . $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 계정과목 삭제 (글로벌 데이터)
|
|
*/
|
|
public function accountCodeDestroy(int $id): JsonResponse
|
|
{
|
|
try {
|
|
$accountCode = AccountCode::find($id);
|
|
|
|
if (!$accountCode) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '계정과목을 찾을 수 없습니다.'
|
|
], 404);
|
|
}
|
|
|
|
$accountCode->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '계정과목이 삭제되었습니다.'
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '삭제 실패: ' . $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 입출금 내역 저장 (계정과목 포함)
|
|
*/
|
|
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;
|
|
$savedUniqueKeys = [];
|
|
|
|
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,
|
|
];
|
|
|
|
// 고유 키 생성 (오버라이드 동기화용)
|
|
$uniqueKey = implode('|', [
|
|
$data['bank_account_num'],
|
|
$transDt,
|
|
(int) $data['deposit'],
|
|
(int) $data['withdraw'],
|
|
(int) $data['balance'],
|
|
]);
|
|
$savedUniqueKeys[] = $uniqueKey;
|
|
|
|
// Upsert: 있으면 업데이트, 없으면 생성
|
|
// balance 포함: 같은 금액이라도 잔액이 다르면 별도 거래로 구분
|
|
$existing = BankTransaction::where('tenant_id', $tenantId)
|
|
->where('bank_account_num', $data['bank_account_num'])
|
|
->where('trans_dt', $transDt)
|
|
->whereRaw('CAST(deposit AS SIGNED) = ?', [(int) $data['deposit']])
|
|
->whereRaw('CAST(withdraw AS SIGNED) = ?', [(int) $data['withdraw']])
|
|
->whereRaw('CAST(balance AS SIGNED) = ?', [(int) $data['balance']])
|
|
->first();
|
|
|
|
if ($existing) {
|
|
// 계정과목 + 적요/예금주명 업데이트 (balance는 키값이므로 제외)
|
|
$existing->update([
|
|
'summary' => $data['summary'],
|
|
'cast' => $data['cast'],
|
|
'trans_office' => $data['trans_office'],
|
|
'account_code' => $data['account_code'],
|
|
'account_name' => $data['account_name'],
|
|
]);
|
|
$updated++;
|
|
} else {
|
|
BankTransaction::create($data);
|
|
$saved++;
|
|
}
|
|
}
|
|
|
|
// 오버라이드 동기화: 메인 테이블에 저장된 값이 최신이므로
|
|
// override의 modified_cast를 제거하여 충돌 방지
|
|
if (!empty($savedUniqueKeys)) {
|
|
$overrides = BankTransactionOverride::forTenant($tenantId)
|
|
->whereIn('unique_key', $savedUniqueKeys)
|
|
->get();
|
|
|
|
foreach ($overrides as $override) {
|
|
if ($override->modified_cast !== null) {
|
|
if (!empty($override->modified_summary)) {
|
|
// summary 오버라이드는 유지, cast 오버라이드만 제거
|
|
$override->update(['modified_cast' => null]);
|
|
} else {
|
|
// summary도 없으면 오버라이드 레코드 삭제
|
|
$override->delete();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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->trans_office,
|
|
$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()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 거래내역 적요/내용 오버라이드 저장
|
|
*/
|
|
public function saveOverride(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
|
|
$validated = $request->validate([
|
|
'uniqueKey' => 'required|string|max:100',
|
|
'modifiedSummary' => 'nullable|string|max:200',
|
|
'modifiedCast' => 'nullable|string|max:200',
|
|
]);
|
|
|
|
$result = BankTransactionOverride::saveOverride(
|
|
$tenantId,
|
|
$validated['uniqueKey'],
|
|
$validated['modifiedSummary'] ?? null,
|
|
$validated['modifiedCast'] ?? null
|
|
);
|
|
|
|
if ($result === null) {
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '오버라이드가 삭제되었습니다.',
|
|
'deleted' => true
|
|
]);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '오버라이드가 저장되었습니다.',
|
|
'data' => [
|
|
'id' => $result->id,
|
|
'modifiedSummary' => $result->modified_summary,
|
|
'modifiedCast' => $result->modified_cast,
|
|
]
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('오버라이드 저장 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '저장 오류: ' . $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 수동 거래 등록
|
|
*/
|
|
public function storeManual(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
|
|
$validated = $request->validate([
|
|
'bank_account_num' => 'required|string',
|
|
'bank_code' => 'nullable|string',
|
|
'bank_name' => 'nullable|string',
|
|
'trans_date' => 'required|date_format:Ymd',
|
|
'trans_time' => 'nullable|string|max:6',
|
|
'deposit' => 'required|numeric',
|
|
'withdraw' => 'required|numeric',
|
|
'balance' => 'nullable|numeric',
|
|
'summary' => 'nullable|string',
|
|
'cast' => 'nullable|string',
|
|
'memo' => 'nullable|string',
|
|
'trans_office' => 'nullable|string',
|
|
'account_code' => 'nullable|string',
|
|
'account_name' => 'nullable|string',
|
|
]);
|
|
|
|
$transTime = $validated['trans_time'] ?? '000000';
|
|
$transDt = $validated['trans_date'] . $transTime;
|
|
|
|
$transaction = BankTransaction::create([
|
|
'tenant_id' => $tenantId,
|
|
'bank_account_num' => $validated['bank_account_num'],
|
|
'bank_code' => $validated['bank_code'] ?? '',
|
|
'bank_name' => $validated['bank_name'] ?? '',
|
|
'trans_date' => $validated['trans_date'],
|
|
'trans_time' => $transTime,
|
|
'trans_dt' => $transDt,
|
|
'deposit' => $validated['deposit'],
|
|
'withdraw' => $validated['withdraw'],
|
|
'balance' => $validated['balance'] ?? 0,
|
|
'summary' => $validated['summary'] ?? '',
|
|
'cast' => $validated['cast'] ?? '',
|
|
'memo' => $validated['memo'] ?? '',
|
|
'trans_office' => $validated['trans_office'] ?? '',
|
|
'account_code' => $validated['account_code'] ?? null,
|
|
'account_name' => $validated['account_name'] ?? null,
|
|
'is_manual' => true,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '수동 거래가 등록되었습니다.',
|
|
'data' => ['id' => $transaction->id]
|
|
]);
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '입력 데이터가 올바르지 않습니다.',
|
|
'errors' => $e->errors()
|
|
], 422);
|
|
} catch (\Throwable $e) {
|
|
Log::error('수동 거래 등록 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '등록 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 수동 거래 수정
|
|
*/
|
|
public function updateManual(Request $request, int $id): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
|
|
$transaction = BankTransaction::where('id', $id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('is_manual', true)
|
|
->firstOrFail();
|
|
|
|
$validated = $request->validate([
|
|
'bank_account_num' => 'required|string',
|
|
'bank_code' => 'nullable|string',
|
|
'bank_name' => 'nullable|string',
|
|
'trans_date' => 'required|date_format:Ymd',
|
|
'trans_time' => 'nullable|string|max:6',
|
|
'deposit' => 'required|numeric',
|
|
'withdraw' => 'required|numeric',
|
|
'balance' => 'nullable|numeric',
|
|
'summary' => 'nullable|string',
|
|
'cast' => 'nullable|string',
|
|
'memo' => 'nullable|string',
|
|
'trans_office' => 'nullable|string',
|
|
'account_code' => 'nullable|string',
|
|
'account_name' => 'nullable|string',
|
|
]);
|
|
|
|
$transTime = $validated['trans_time'] ?? '000000';
|
|
$transDt = $validated['trans_date'] . $transTime;
|
|
|
|
$transaction->update([
|
|
'bank_account_num' => $validated['bank_account_num'],
|
|
'bank_code' => $validated['bank_code'] ?? '',
|
|
'bank_name' => $validated['bank_name'] ?? '',
|
|
'trans_date' => $validated['trans_date'],
|
|
'trans_time' => $transTime,
|
|
'trans_dt' => $transDt,
|
|
'deposit' => $validated['deposit'],
|
|
'withdraw' => $validated['withdraw'],
|
|
'balance' => $validated['balance'] ?? 0,
|
|
'summary' => $validated['summary'] ?? '',
|
|
'cast' => $validated['cast'] ?? '',
|
|
'memo' => $validated['memo'] ?? '',
|
|
'trans_office' => $validated['trans_office'] ?? '',
|
|
'account_code' => $validated['account_code'] ?? null,
|
|
'account_name' => $validated['account_name'] ?? null,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '수동 거래가 수정되었습니다.',
|
|
]);
|
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.'
|
|
], 404);
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '입력 데이터가 올바르지 않습니다.',
|
|
'errors' => $e->errors()
|
|
], 422);
|
|
} catch (\Throwable $e) {
|
|
Log::error('수동 거래 수정 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '수정 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 수동 거래 삭제
|
|
*/
|
|
public function destroyManual(int $id): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
|
|
$transaction = BankTransaction::where('id', $id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('is_manual', true)
|
|
->firstOrFail();
|
|
|
|
$transaction->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '수동 거래가 삭제되었습니다.',
|
|
]);
|
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.'
|
|
], 404);
|
|
} catch (\Throwable $e) {
|
|
Log::error('수동 거래 삭제 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '삭제 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 분개 내역 조회
|
|
*/
|
|
public function splits(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$startDate = $request->input('startDate', date('Ymd'));
|
|
$endDate = $request->input('endDate', date('Ymd'));
|
|
|
|
$splits = BankTransactionSplit::getByDateRange($tenantId, $startDate, $endDate);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $splits
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('계좌 분개 내역 조회 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '조회 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 분개 저장
|
|
*/
|
|
public function saveSplits(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$uniqueKey = $request->input('uniqueKey');
|
|
$originalData = $request->input('originalData', []);
|
|
$splits = $request->input('splits', []);
|
|
|
|
if (empty($uniqueKey)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '고유키가 없습니다.'
|
|
]);
|
|
}
|
|
|
|
// 분개 금액 합계 검증
|
|
$originalAmount = floatval($originalData['originalAmount'] ?? 0);
|
|
$splitTotal = array_sum(array_map(function ($s) {
|
|
return floatval($s['amount'] ?? 0);
|
|
}, $splits));
|
|
|
|
if (abs($originalAmount - $splitTotal) > 0.01) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => "분개 금액 합계({$splitTotal})가 원본 금액({$originalAmount})과 일치하지 않습니다."
|
|
]);
|
|
}
|
|
|
|
DB::beginTransaction();
|
|
|
|
BankTransactionSplit::saveSplits($tenantId, $uniqueKey, $originalData, $splits);
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '분개가 저장되었습니다.',
|
|
'splitCount' => count($splits)
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
Log::error('계좌 분개 저장 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '저장 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 분개 삭제 (원본으로 복원)
|
|
*/
|
|
public function deleteSplits(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$uniqueKey = $request->input('uniqueKey');
|
|
|
|
if (empty($uniqueKey)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '고유키가 없습니다.'
|
|
]);
|
|
}
|
|
|
|
$deleted = BankTransactionSplit::deleteSplits($tenantId, $uniqueKey);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '분개가 삭제되었습니다.',
|
|
'deleted' => $deleted
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('계좌 분개 삭제 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '삭제 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 병합된 로그에서 수동입력 건의 잔액을 직전 거래 기준으로 재계산
|
|
* 로그는 날짜 내림차순(DESC) 정렬 상태로 전달됨
|
|
*/
|
|
/**
|
|
* 조회기간 직전의 마지막 잔액 조회
|
|
* API 데이터는 잔액이 정확하므로 그대로 사용,
|
|
* 수동입력은 잔액이 0일 수 있으므로 입출금 누적으로 계산
|
|
*/
|
|
private function findBaseBalance(int $tenantId, string $startDate, ?string $bankAccountNum = null): ?float
|
|
{
|
|
// 조회기간 이전의 모든 거래를 시간순(ASC)으로 조회
|
|
$prevTransactions = BankTransaction::where('tenant_id', $tenantId)
|
|
->where('trans_date', '<', $startDate)
|
|
->when($bankAccountNum, fn($q) => $q->where('bank_account_num', $bankAccountNum))
|
|
->orderBy('trans_date', 'asc')
|
|
->orderBy('trans_time', 'asc')
|
|
->get();
|
|
|
|
if ($prevTransactions->isEmpty()) return null;
|
|
|
|
// 시간순으로 순회하며 잔액 추적
|
|
$balance = null;
|
|
foreach ($prevTransactions as $tx) {
|
|
if (!$tx->is_manual && (float) $tx->balance != 0) {
|
|
// API 데이터: 바로빌이 제공한 정확한 잔액 사용
|
|
$balance = (float) $tx->balance;
|
|
} else {
|
|
// 수동입력 또는 잔액 0인 건: 이전 잔액에서 입출금 계산
|
|
$prev = $balance ?? 0;
|
|
$balance = $prev + (float) $tx->deposit - (float) $tx->withdraw;
|
|
}
|
|
}
|
|
|
|
return $balance;
|
|
}
|
|
|
|
private function recalcManualBalances(array $logs, ?float $baseBalance = null): array
|
|
{
|
|
if (empty($logs)) return $logs;
|
|
|
|
// 시간순(ASC)으로 뒤집어서 순차 처리
|
|
$logs = array_reverse($logs);
|
|
|
|
$prevBalance = $baseBalance;
|
|
foreach ($logs as &$log) {
|
|
if (!empty($log['isManual'])) {
|
|
$deposit = (float) ($log['deposit'] ?? 0);
|
|
$withdraw = (float) ($log['withdraw'] ?? 0);
|
|
$newBalance = ($prevBalance !== null ? $prevBalance : 0) + $deposit - $withdraw;
|
|
$log['balance'] = $newBalance;
|
|
$log['balanceFormatted'] = number_format($newBalance);
|
|
}
|
|
$prevBalance = (float) ($log['balance'] ?? 0);
|
|
}
|
|
unset($log);
|
|
|
|
// 다시 내림차순(DESC)으로 복원
|
|
return array_reverse($logs);
|
|
}
|
|
|
|
/**
|
|
* 수동 입력 건을 API 로그 형식으로 변환
|
|
*/
|
|
private function convertManualToLogs($manualTransactions): array
|
|
{
|
|
$logs = [];
|
|
$totalDeposit = 0;
|
|
$totalWithdraw = 0;
|
|
|
|
if (!$manualTransactions || $manualTransactions->isEmpty()) {
|
|
return [
|
|
'logs' => [],
|
|
'summary' => [
|
|
'totalDeposit' => 0,
|
|
'totalWithdraw' => 0,
|
|
'count' => 0,
|
|
]
|
|
];
|
|
}
|
|
|
|
foreach ($manualTransactions as $t) {
|
|
$deposit = (float) $t->deposit;
|
|
$withdraw = (float) $t->withdraw;
|
|
$balance = (float) $t->balance;
|
|
$totalDeposit += $deposit;
|
|
$totalWithdraw += $withdraw;
|
|
|
|
$transDt = $t->trans_dt ?? '';
|
|
$dateTime = '';
|
|
if (!empty($transDt) && strlen($transDt) >= 8) {
|
|
$dateTime = substr($transDt, 0, 4) . '-' . substr($transDt, 4, 2) . '-' . substr($transDt, 6, 2);
|
|
if (strlen($transDt) >= 14) {
|
|
$dateTime .= ' ' . substr($transDt, 8, 2) . ':' . substr($transDt, 10, 2) . ':' . substr($transDt, 12, 2);
|
|
}
|
|
}
|
|
|
|
$uniqueKey = implode('|', [
|
|
$t->bank_account_num,
|
|
$transDt,
|
|
(int) $deposit,
|
|
(int) $withdraw,
|
|
(int) $balance,
|
|
]);
|
|
|
|
$logs[] = [
|
|
'transDate' => $t->trans_date,
|
|
'transTime' => $t->trans_time,
|
|
'transDateTime' => $dateTime,
|
|
'bankAccountNum' => $t->bank_account_num,
|
|
'bankName' => $t->bank_name ?? '',
|
|
'deposit' => $deposit,
|
|
'withdraw' => $withdraw,
|
|
'depositFormatted' => number_format($deposit),
|
|
'withdrawFormatted' => number_format($withdraw),
|
|
'balance' => $balance,
|
|
'balanceFormatted' => number_format($balance),
|
|
'summary' => $t->summary ?? '',
|
|
'originalSummary' => $t->summary ?? '',
|
|
'cast' => $t->cast ?? '',
|
|
'originalCast' => $t->cast ?? '',
|
|
'memo' => $t->memo ?? '',
|
|
'transOffice' => $t->trans_office ?? '',
|
|
'accountCode' => $t->account_code ?? '',
|
|
'accountName' => $t->account_name ?? '',
|
|
'isSaved' => true,
|
|
'isOverridden' => false,
|
|
'isManual' => true,
|
|
'dbId' => $t->id,
|
|
'uniqueKey' => $uniqueKey,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'logs' => $logs,
|
|
'summary' => [
|
|
'totalDeposit' => $totalDeposit,
|
|
'totalWithdraw' => $totalWithdraw,
|
|
'count' => count($logs),
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* SOAP 호출
|
|
*/
|
|
private function callSoap(string $method, array $params = []): array
|
|
{
|
|
if (!$this->soapClient) {
|
|
return [
|
|
'success' => false,
|
|
'error' => '바로빌 SOAP 클라이언트가 초기화되지 않았습니다.'
|
|
];
|
|
}
|
|
|
|
if (empty($this->certKey) && !$this->isTestMode) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'CERTKEY가 설정되지 않았습니다.'
|
|
];
|
|
}
|
|
|
|
if (empty($this->corpNum)) {
|
|
return [
|
|
'success' => false,
|
|
'error' => '사업자번호가 설정되지 않았습니다.'
|
|
];
|
|
}
|
|
|
|
// CERTKEY와 CorpNum 자동 추가
|
|
if (!isset($params['CERTKEY'])) {
|
|
$params['CERTKEY'] = $this->certKey ?? '';
|
|
}
|
|
if (!isset($params['CorpNum'])) {
|
|
$params['CorpNum'] = $this->corpNum;
|
|
}
|
|
|
|
try {
|
|
Log::info("바로빌 계좌 API 호출 - Method: {$method}, CorpNum: {$this->corpNum}");
|
|
|
|
$result = $this->soapClient->$method($params);
|
|
$resultProperty = $method . 'Result';
|
|
|
|
if (isset($result->$resultProperty)) {
|
|
$resultData = $result->$resultProperty;
|
|
|
|
// 에러 코드 체크
|
|
if (is_numeric($resultData) && $resultData < 0) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->getErrorMessage((int)$resultData),
|
|
'error_code' => (int)$resultData
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => $resultData
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => $result
|
|
];
|
|
} catch (\SoapFault $e) {
|
|
Log::error('바로빌 SOAP 오류: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'error' => 'SOAP 오류: ' . $e->getMessage(),
|
|
'error_code' => $e->getCode()
|
|
];
|
|
} catch (\Throwable $e) {
|
|
Log::error('바로빌 API 호출 오류: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'error' => 'API 호출 오류: ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
}
|