diff --git a/barobill/index.php b/barobill/index.php index 6f2e83c..089f05f 100644 --- a/barobill/index.php +++ b/barobill/index.php @@ -61,7 +61,31 @@

바로빌 API 회계 솔루션

- + +
+ + 💰 계좌조회 + + + 💳 카드내역 + + + 🏢 테넌트관리 + + + 👥 회원관리 + + + 📖 API정보 + + +
+ + + 📄 세금계산서 + + +
diff --git a/barobill_registration/index.php b/barobill_registration/index.php index 439dbf6..9c94105 100644 --- a/barobill_registration/index.php +++ b/barobill_registration/index.php @@ -56,14 +56,32 @@

바로빌 회원관리 솔루션

-
- +
+ + 계좌조회 + + + 카드내역 + + + 테넌트관리 + + + 회원관리 + + + API정보 + + +
+ + 현황 - + 세금계산서 - +
diff --git a/company/peoplelife/index.php b/company/peoplelife/index.php new file mode 100644 index 0000000..ebac30d --- /dev/null +++ b/company/peoplelife/index.php @@ -0,0 +1,520 @@ + + + + + + 피플라이프(PeopleLife) 기업 분석 리포트 + + + + + + + + + + + + + +
+
+
+
+
P
+ 피플라이프 기업분석 +
+ +
+
+ +
+ + + + +
+
+ +
+ + +
+ +
+ +
+ + + + + + \ No newline at end of file diff --git a/eaccount/README.md b/eaccount/README.md new file mode 100644 index 0000000..946f3b9 --- /dev/null +++ b/eaccount/README.md @@ -0,0 +1,138 @@ +# 법인카드 사용내역 조회 모듈 + +바로빌 API를 이용한 법인카드 사용내역 조회 모듈입니다. + +## 📋 기능 + +- 등록된 카드 목록 조회 +- 기간별/일별/월별 카드 사용내역 조회 +- 사용금액 통계 (총 사용금액, 사용건수, 취소건수) +- 페이지네이션 지원 + +## 🔧 설정 + +### 1. API 키 설정 (기존 etax 모듈과 공유) + +다음 파일들이 필요합니다 (`/apikey/` 폴더): + +| 파일명 | 설명 | 예시 | +|--------|------|------| +| `barobill_cert_key.txt` | 바로빌 CERTKEY (인증서 키) | `ABC123...` | +| `barobill_corp_num.txt` | 사업자번호 (하이픈 제외) | `6648603713` | +| `barobill_test_mode.txt` | 테스트 모드 (선택) | `test` 또는 `true` | + +### 2. 바로빌 카드 등록 + +카드 사용내역을 조회하려면 **바로빌 웹사이트**에서 카드를 먼저 등록해야 합니다. + +1. [바로빌](https://www.barobill.co.kr) 로그인 +2. 카드조회 서비스 신청 +3. 카드 등록 (카드사 웹 ID/비밀번호 필요) + +## 📁 파일 구조 + +``` +ecard/ +├── index.php # 메인 UI (React 기반) +├── api/ +│ ├── barobill_card_config.php # 바로빌 카드 API 설정 +│ ├── cards.php # 등록된 카드 목록 API +│ └── usage.php # 카드 사용내역 조회 API +└── README.md # 이 문서 +``` + +## 🔌 API 엔드포인트 + +### 카드 목록 조회 +``` +GET /ecard/api/cards.php +``` + +**응답 예시:** +```json +{ + "success": true, + "cards": [ + { + "cardNum": "1234-****-****-5678", + "cardCompany": "02", + "cardCompanyName": "KB국민", + "alias": "법인카드1", + "status": "1", + "statusName": "정상" + } + ], + "count": 1 +} +``` + +### 사용내역 조회 +``` +GET /ecard/api/usage.php?type=period&startDate=20241101&endDate=20241130 +``` + +**파라미터:** +| 파라미터 | 설명 | 기본값 | +|---------|------|--------| +| `type` | 조회 타입 (period/daily/monthly) | `period` | +| `cardNum` | 카드번호 (빈값=전체) | - | +| `startDate` | 시작일 (YYYYMMDD) - period용 | 30일 전 | +| `endDate` | 종료일 (YYYYMMDD) - period용 | 오늘 | +| `baseDate` | 기준일 (YYYYMMDD) - daily용 | 오늘 | +| `baseMonth` | 기준월 (YYYYMM) - monthly용 | 이번달 | +| `page` | 페이지 번호 | `1` | +| `limit` | 페이지당 건수 | `50` | + +**응답 예시:** +```json +{ + "success": true, + "data": { + "logs": [ + { + "cardNum": "1234-****-****-5678", + "approvalNum": "12345678", + "approvalDate": "2024-11-15", + "approvalTime": "14:30:25", + "merchantName": "스타벅스 강남점", + "amount": 5000, + "totalAmountFormatted": "5,000", + "approvalTypeName": "승인", + "installmentName": "일시불" + } + ], + "pagination": { + "currentPage": 1, + "countPerPage": 50, + "maxPageNum": 1, + "totalCount": 15 + }, + "summary": { + "totalAmount": 150000, + "count": 15, + "approvalCount": 14, + "cancelCount": 1 + } + } +} +``` + +## 🎨 UI 기능 + +- **카드 선택**: 특정 카드 또는 전체 카드 조회 +- **기간 설정**: 날짜 범위 직접 선택 또는 빠른 선택 (오늘, 7일, 30일, 3개월, 6개월) +- **통계 대시보드**: 총 사용금액, 사용건수, 취소건수 표시 +- **사용내역 테이블**: 승인일시, 가맹점명, 금액, 할부, 승인/취소 구분 + +## ⚠️ 주의사항 + +1. 바로빌 카드조회 서비스는 **유료 서비스**입니다. +2. 카드 등록 시 **카드사 웹 ID/비밀번호**가 필요합니다. +3. 카드사에서 데이터를 수집하므로 **실시간 조회가 아닐 수 있습니다** (보통 1일 1회 수집). +4. 테스트 환경에서는 실제 데이터가 아닌 테스트 데이터가 조회됩니다. + +## 🔗 참고 문서 + +- [바로빌 카드조회 API 레퍼런스](https://dev.barobill.co.kr/docs/references/카드조회-API) +- [바로빌 개발자센터](https://dev.barobill.co.kr) + diff --git a/eaccount/api/account_status.php b/eaccount/api/account_status.php new file mode 100644 index 0000000..79fa3d6 --- /dev/null +++ b/eaccount/api/account_status.php @@ -0,0 +1,340 @@ +load(); + +require_once('barobill_account_config.php'); +require_once(getenv('DOCUMENT_ROOT') . '/session.php'); +require_once(getenv('DOCUMENT_ROOT') . '/lib/mydb.php'); + +try { + // 1. 로컬 DB의 company_accounts 테이블에서 계좌 정보 가져오기 + $localAccounts = []; + $selectedTenantId = $_SESSION['eaccount_tenant_id'] ?? null; + + if ($selectedTenantId) { + try { + $pdo = db_connect(); + if ($pdo) { + $sql = "SELECT id, company_id, bank_code, account_num, account_pwd + FROM {$DB}.company_accounts + WHERE company_id = ? + ORDER BY id DESC"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$selectedTenantId]); + $localAccounts = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + } catch (Exception $e) { + error_log('로컬 계좌 정보 로드 실패: ' . $e->getMessage()); + } + } + + // 2. 바로빌 API에서 계좌 정보 가져오기 (시도) + $barobillAccounts = []; + $barobillError = null; + $barobillErrorCode = null; + $debugInfo = null; + + $result = callBarobillAccountSOAP('GetBankAccountEx', [ + 'AvailOnly' => 0 // 전체 계좌 조회 (0: 전체, 1: 사용가능, 2: 해지) + ]); + + if ($result['success']) { + $data = $result['data']; + $accountList = []; + + // 디버그: 응답 구조 확인 + $debugInfo = [ + 'data_type' => gettype($data), + 'data_keys' => is_object($data) ? array_keys(get_object_vars($data)) : [], + 'has_BankAccountEx' => isset($data->BankAccountEx), + 'has_BankAccount' => isset($data->BankAccount), + 'BankAccountEx_type' => isset($data->BankAccountEx) ? gettype($data->BankAccountEx) : 'N/A', + 'BankAccount_type' => isset($data->BankAccount) ? gettype($data->BankAccount) : 'N/A', + 'BankAccountEx_is_array' => isset($data->BankAccountEx) ? is_array($data->BankAccountEx) : false, + 'BankAccount_is_array' => isset($data->BankAccount) ? is_array($data->BankAccount) : false + ]; + + // 실제 SOAP 응답 구조 확인: + // GetBankAccountExResult -> BankAccount (단일 객체 또는 배열) + // 또는 BankAccountEx (배열) - 다른 API 버전일 수 있음 + + // 우선순위 1: BankAccount 확인 (실제 응답 구조 - SOAP XML에서 확인됨) + if (isset($data->BankAccount)) { + if (is_array($data->BankAccount)) { + $accountList = $data->BankAccount; + } else if (is_object($data->BankAccount)) { + // 단일 객체인 경우 배열로 변환 + $accountList = [$data->BankAccount]; + } + } + // 우선순위 2: BankAccountEx 배열 확인 (다른 API 버전) + else if (isset($data->BankAccountEx)) { + // 단일 객체가 에러 코드인 경우 (예: -10002, -25001) + if (is_numeric($data->BankAccountEx) && $data->BankAccountEx < 0) { + $errorCode = $data->BankAccountEx; + $barobillError = '바로빌 API 오류: ' . $errorCode; + $barobillErrorCode = $errorCode; + + // 상세 에러 메시지 매핑 + if ($errorCode == -10002) { + $barobillError = '인증 실패 (-10002). CERTKEY 또는 사업자번호를 확인해주세요.'; + } else if ($errorCode == -25001) { + $barobillError = '등록된 계좌가 없습니다 (-25001). 바로빌 사이트에서 계좌를 등록해주세요.'; + } else if ($errorCode == -50214) { + $barobillError = '은행 로그인 실패 (-50214). 바로빌 사이트에서 계좌 비밀번호/인증서를 점검해주세요.'; + } + } + // 배열 또는 객체인 경우 + else if (is_array($data->BankAccountEx)) { + $accountList = $data->BankAccountEx; + } else if (is_object($data->BankAccountEx)) { + // 단일 객체인 경우 배열로 변환 + $accountList = [$data->BankAccountEx]; + } + } + // 방법 3: 직접 BankAccount 속성 확인 (다른 구조일 수 있음) + else { + // 객체의 모든 속성을 확인하여 BankAccount 관련 속성 찾기 + if (is_object($data)) { + $vars = get_object_vars($data); + foreach ($vars as $key => $value) { + // BankAccount로 시작하는 속성 찾기 + if (stripos($key, 'BankAccount') !== false) { + if (is_array($value)) { + $accountList = $value; + break; + } else if (is_object($value)) { + $accountList = [$value]; + break; + } + } + } + } + } + + // 계좌 정보 파싱 + foreach ($accountList as $acc) { + // 객체가 아닌 경우 스킵 + if (!is_object($acc)) { + continue; + } + + // 에러 코드 체크 (개별 계좌 레벨) + if (isset($acc->BankAccountNum)) { + // BankAccountNum이 음수인 경우 에러 코드 + if (is_numeric($acc->BankAccountNum) && $acc->BankAccountNum < 0) { + $errorCode = $acc->BankAccountNum; + if (!$barobillError) { + $barobillError = '바로빌 API 오류: ' . $errorCode; + $barobillErrorCode = $errorCode; + } + continue; + } + // BankAccountNum이 비어있는 경우도 스킵 + if (empty($acc->BankAccountNum)) { + continue; + } + } else { + // BankAccountNum이 없는 경우도 스킵 + continue; + } + + // BankName으로 BankCode 추론 (응답에 BankCode가 없는 경우) + $bankCode = $acc->BankCode ?? ''; + if (empty($bankCode) && isset($acc->BankName)) { + // BankName으로 BankCode 찾기 + $bankName = $acc->BankName; + $bankCodeMap = [ + '기업은행' => '003', + 'IBK기업은행' => '003', + 'KB국민은행' => '004', + '국민은행' => '004', + '우리은행' => '020', + '신한은행' => '088', + '하나은행' => '081', + 'NH농협은행' => '011', + '농협은행' => '011' + ]; + $bankCode = $bankCodeMap[$bankName] ?? ''; + } + + // UseState 처리: 없으면 기본값 1 (사용중)으로 설정 + // UseState: 1=사용중, 0=중지, 2=해지 + $useState = isset($acc->UseState) ? intval($acc->UseState) : 1; // 기본값: 사용중 + + $barobillAccounts[] = [ + 'bankAccountNum' => $acc->BankAccountNum ?? '', + 'bankCode' => $bankCode, + 'bankName' => getBankName($bankCode) ?: ($acc->BankName ?? ''), + 'accountName' => $acc->AccountName ?? '', + 'accountType' => $acc->AccountType ?? '', + 'currency' => $acc->Currency ?? 'KRW', + 'issueDate' => $acc->IssueDate ?? '', + 'balance' => $acc->Balance ?? 0, + 'status' => $useState, + 'statusText' => $useState == 1 ? '사용중' : ($useState == 0 ? '중지' : ($useState == 2 ? '해지' : '알 수 없음')), + 'source' => 'barobill_api' // 바로빌 API에서 가져온 정보 + ]; + } + + // 디버그 정보 추가 + $debugInfo['account_count'] = count($barobillAccounts); + $debugInfo['account_list'] = array_map(function($acc) { + return [ + 'bankAccountNum' => $acc['bankAccountNum'], + 'bankCode' => $acc['bankCode'], + 'accountName' => $acc['accountName'] + ]; + }, $barobillAccounts); + + } else { + $barobillError = $result['error']; + $barobillErrorCode = $result['error_code'] ?? null; + } + + // 3. 로컬 DB 계좌 정보를 바로빌 계좌 정보와 매칭하여 통합 + $allAccounts = []; + + // 먼저 바로빌 API 계좌 정보를 기준으로 추가 (실제 사용 가능한 계좌) + foreach ($barobillAccounts as $barobillAcc) { + $allAccounts[] = $barobillAcc; + } + + // 로컬 DB 계좌 정보를 바로빌 API 계좌와 매칭 + foreach ($localAccounts as $localAcc) { + $matched = false; + // 바로빌 API에 같은 계좌번호가 있는지 확인 + foreach ($allAccounts as &$existingAcc) { + // 계좌번호 매칭 (하이픈 제거 후 비교) + $localAccountNum = str_replace('-', '', $localAcc['account_num']); + $barobillAccountNum = str_replace('-', '', $existingAcc['bankAccountNum']); + + if ($localAccountNum === $barobillAccountNum) { + // 바로빌 API 계좌 정보에 로컬 DB 정보 병합 + $existingAcc['id'] = $localAcc['id']; + $existingAcc['hasPassword'] = !empty($localAcc['account_pwd']); + $existingAcc['source'] = 'both'; // 양쪽 모두에 있음 (실제 사용 가능) + $matched = true; + break; + } + } + + // 바로빌 API에 없는 로컬 계좌는 경고와 함께 추가 + if (!$matched) { + $isApiError = !empty($barobillError); + $statusText = $isApiError ? '상태 확인 불가' : '바로빌 미등록'; + $sourceText = $isApiError ? 'barobill_api_error' : 'local_db_only'; + + $allAccounts[] = [ + 'id' => $localAcc['id'], + 'bankAccountNum' => $localAcc['account_num'], + 'bankCode' => $localAcc['bank_code'], + 'bankName' => getBankName($localAcc['bank_code']), + 'accountName' => '', // 로컬 DB에는 별칭 정보 없음 + 'accountType' => '', + 'currency' => 'KRW', + 'issueDate' => '', + 'balance' => 0, + 'status' => '', + 'statusText' => $statusText, + 'source' => $sourceText, // 상태 확인 필요 + 'hasPassword' => !empty($localAcc['account_pwd']), + 'warning' => true, // 경고 표시용 + 'api_error' => $isApiError // 프론트엔드에서 구분하기 위함 + ]; + } + } + + // 사용 가능한 계좌 수 계산 (바로빌 API에서 확인된 계좌) + $availableAccounts = array_filter($allAccounts, function($acc) { + return $acc['source'] === 'barobill_api' || $acc['source'] === 'both'; + }); + $availableCount = count($availableAccounts); + + // 경고가 필요한 계좌 수 (로컬에만 있는 계좌) + $warningAccounts = array_filter($allAccounts, function($acc) { + return isset($acc['warning']) && $acc['warning'] === true; + }); + $warningCount = count($warningAccounts); + + $response = [ + 'success' => true, + 'accounts' => $allAccounts, + 'count' => count($allAccounts), + 'available_count' => $availableCount, // 바로빌 API에서 확인된 사용 가능한 계좌 수 + 'warning_count' => $warningCount, // 로컬에만 있는 계좌 수 + 'local_count' => count($localAccounts), + 'barobill_count' => count($barobillAccounts), + 'message' => $availableCount > 0 + ? '사용 가능한 계좌가 ' . $availableCount . '개 있습니다.' . ($warningCount > 0 ? ' (바로빌 미등록 계좌 ' . $warningCount . '개)' : '') + : '사용 가능한 계좌가 없습니다.' . ($warningCount > 0 ? ' (로컬에만 등록된 계좌 ' . $warningCount . '개는 바로빌 API에 등록이 필요합니다)' : '') + ]; + + // 바로빌 API 오류 정보 추가 + if ($barobillError) { + $response['barobill_error'] = $barobillError; + $response['barobill_error_code'] = $barobillErrorCode; + } + + // 디버그 정보 추가 + if (isset($result['debug'])) { + $response['debug'] = $result['debug']; + } + + // API 응답 구조 디버그 정보 추가 + if (isset($debugInfo)) { + $response['api_debug'] = $debugInfo; + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage(), + 'accounts' => [], + 'count' => 0 + ], JSON_UNESCAPED_UNICODE); +} + +/** + * 은행 코드 -> 은행명 변환 + */ +function getBankName($code) { + $banks = [ + '002' => 'KDB산업은행', + '003' => 'IBK기업은행', + '004' => 'KB국민은행', + '007' => '수협은행', + '011' => 'NH농협은행', + '012' => '지역농축협', + '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; +} +?> + diff --git a/eaccount/api/accounts.php b/eaccount/api/accounts.php new file mode 100644 index 0000000..505dd71 --- /dev/null +++ b/eaccount/api/accounts.php @@ -0,0 +1,245 @@ + $availOnly + ]); + + if ($result['success']) { + $accounts = []; + $data = $result['data']; + + // 에러 코드 체크 (전체 응답 레벨) + if (isset($data->BankAccountEx)) { + // 단일 객체가 에러 코드인 경우 + if (is_numeric($data->BankAccountEx) && $data->BankAccountEx < 0) { + $errorCode = $data->BankAccountEx; + $errorMsg = '계좌 목록 조회 실패: ' . $errorCode; + + // 상세 에러 메시지 매핑 + if ($errorCode == -50214) { + $errorMsg = '은행 로그인 실패 (-50214). 바로빌 사이트에서 계좌 비밀번호/인증서를 점검해주세요.'; + } else if ($errorCode == -24005) { + $errorMsg = '사용자 정보 불일치 (-24005). 사업자번호를 확인해주세요.'; + } else if ($errorCode == -25001) { + $errorMsg = '등록된 계좌가 없습니다 (-25001). 바로빌 사이트에서 계좌를 등록해주세요.'; + } + + echo json_encode([ + 'success' => false, + 'error' => $errorMsg, + 'error_code' => $errorCode + ], JSON_UNESCAPED_UNICODE); + return; + } + } + + // 실제 SOAP 응답 구조 확인: + // GetBankAccountExResult -> BankAccount (단일 객체 또는 배열) + // 또는 BankAccountEx (배열) - 다른 API 버전일 수 있음 + + $accountList = []; + + // 우선순위 1: BankAccount 확인 (실제 응답 구조) + if (isset($data->BankAccount)) { + if (is_array($data->BankAccount)) { + $accountList = $data->BankAccount; + } else if (is_object($data->BankAccount)) { + // 단일 객체인 경우 배열로 변환 + $accountList = [$data->BankAccount]; + } + } + // 우선순위 2: BankAccountEx 배열 확인 (다른 API 버전) + else if (isset($data->BankAccountEx)) { + if (is_array($data->BankAccountEx)) { + $accountList = $data->BankAccountEx; + } else if (is_object($data->BankAccountEx)) { + // 단일 객체인 경우 배열로 변환 + $accountList = [$data->BankAccountEx]; + } + } + + foreach ($accountList as $acc) { + // 객체가 아닌 경우 스킵 + if (!is_object($acc)) { + continue; + } + + // 에러 코드 체크 (개별 계좌 레벨) + if (isset($acc->BankAccountNum)) { + // BankAccountNum이 음수인 경우 에러 코드 + if (is_numeric($acc->BankAccountNum) && $acc->BankAccountNum < 0) { + continue; + } + // BankAccountNum이 비어있는 경우도 스킵 + if (empty($acc->BankAccountNum)) { + continue; + } + } else { + // BankAccountNum이 없는 경우도 스킵 + continue; + } + + // BankName으로 BankCode 추론 (응답에 BankCode가 없는 경우) + $bankCode = $acc->BankCode ?? ''; + if (empty($bankCode) && isset($acc->BankName)) { + // BankName으로 BankCode 찾기 + $bankName = $acc->BankName; + $bankCodeMap = [ + '기업은행' => '003', + 'IBK기업은행' => '003', + 'KB국민은행' => '004', + '국민은행' => '004', + '우리은행' => '020', + '신한은행' => '088', + '하나은행' => '081', + 'NH농협은행' => '011', + '농협은행' => '011' + ]; + $bankCode = $bankCodeMap[$bankName] ?? ''; + } + + // UseState 처리: 없으면 기본값 1 (사용중)으로 설정 + $useState = isset($acc->UseState) ? intval($acc->UseState) : 1; + + $accounts[] = [ + 'bankAccountNum' => $acc->BankAccountNum ?? '', + 'bankCode' => $bankCode, + 'bankName' => getBankName($bankCode) ?: ($acc->BankName ?? ''), + 'accountName' => $acc->AccountName ?? '', // 계좌 별칭/이름 + 'accountType' => $acc->AccountType ?? '', // 1:입출금, 2:예적금 + 'currency' => $acc->Currency ?? 'KRW', + 'issueDate' => $acc->IssueDate ?? '', + 'balance' => $acc->Balance ?? 0, + 'status' => $useState // 1:사용, 0:중지, 2:해지 + ]; + } + + $response = [ + 'success' => true, + 'accounts' => $accounts, + 'count' => count($accounts) + ]; + + // 디버그 정보 추가 + if (isset($result['debug'])) { + $response['debug'] = $result['debug']; + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + } else { + // API 호출 실패 시 (예: SoapClient 미설치, 통신 등) 로컬 DB에서 조회 + error_log('바로빌 API 호출 실패, 로컬 DB 조회 시도: ' . $result['error']); + + require_once(getenv('DOCUMENT_ROOT') . '/session.php'); + require_once(getenv('DOCUMENT_ROOT') . '/lib/mydb.php'); + + $accounts = []; + $selectedTenantId = $_SESSION['eaccount_tenant_id'] ?? null; + + if ($selectedTenantId) { + try { + $pdo = db_connect(); + if ($pdo) { + // 로컬 DB에서 계좌 정보 조회 + $sql = "SELECT id, company_id, bank_code, account_num, account_pwd + FROM {$DB}.company_accounts + WHERE company_id = ? + ORDER BY id DESC"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$selectedTenantId]); + $localAccounts = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($localAccounts as $acc) { + // 은행명 변환 + $bankName = getBankName($acc['bank_code']); + + $accounts[] = [ + 'bankAccountNum' => $acc['account_num'], + 'bankCode' => $acc['bank_code'], + 'bankName' => $bankName, + 'accountName' => $bankName . ' ' . $acc['account_num'], + 'accountType' => '', // 로컬 정보 없음 + 'currency' => 'KRW', + 'issueDate' => '', + 'balance' => 0, // 잔액 정보 없음 + 'status' => 1, // 기본값: 사용중 + 'source' => 'local_db_fallback', + 'error_message' => 'API 연동 실패로 로컬 데이터 표시' + ]; + } + } + } catch (Exception $dbEx) { + error_log('로컬 DB 조회 실패: ' . $dbEx->getMessage()); + } + } + + // 로컬 데이터가 있으면 성공으로 masquerade + if (!empty($accounts)) { + echo json_encode([ + 'success' => true, + 'accounts' => $accounts, + 'count' => count($accounts), + 'message' => '바로빌 API 연동에 실패하여 로컬 저장된 계좌 목록을 표시합니다.', + 'api_error' => $result['error'] + ], JSON_UNESCAPED_UNICODE); + } else { + // 로컬 데이터도 없으면 에러 리턴 + echo json_encode([ + 'success' => false, + 'error' => $result['error'], + 'error_code' => $result['error_code'] ?? null + ], JSON_UNESCAPED_UNICODE); + } + } +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} + +/** + * 은행 코드 -> 은행명 변환 + */ +function getBankName($code) { + $banks = [ + '002' => 'KDB산업은행', + '003' => 'IBK기업은행', + '004' => 'KB국민은행', + '007' => '수협은행', + '011' => 'NH농협은행', + '012' => '지역농축협', + '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; +} +?> diff --git a/eaccount/api/barobill_account_config.php b/eaccount/api/barobill_account_config.php new file mode 100644 index 0000000..a1053c7 --- /dev/null +++ b/eaccount/api/barobill_account_config.php @@ -0,0 +1,366 @@ +load(); + +// 인증서 키(CERTKEY) 파일 경로 +$documentRoot = getenv('DOCUMENT_ROOT'); +$certKeyFile = $documentRoot . '/apikey/barobill_cert_key.txt'; +$legacyApiKeyFile = $documentRoot . '/apikey/barobill_api_key.txt'; +$corpNumFile = $documentRoot . '/apikey/barobill_corp_num.txt'; +$testModeFile = $documentRoot . '/apikey/barobill_test_mode.txt'; + +// CERTKEY 읽기 +$barobillCertKey = ''; +if (file_exists($certKeyFile)) { + $content = trim(file_get_contents($certKeyFile)); + + // 설명 텍스트 필터링: 실제 CERTKEY만 추출 + // 설명 텍스트 패턴 체크 + $isPlaceholder = false; + $placeholderPatterns = [ + '/^\[여기에/', // [여기에로 시작 + '/^=/', // =로 시작 + '/바로빌 CERTKEY/', // '바로빌 CERTKEY' 문자열 포함 + '/================================/', // 구분선 포함 + '/설정 방법:/', // '설정 방법:' 포함 + '/인증서 관리/', // '인증서 관리' 포함 + '/개발자센터/', // '개발자센터' 포함 + '/⚠️/', // 경고 이모지 포함 + '/참고:/', // '참고:' 포함 + ]; + + foreach ($placeholderPatterns as $pattern) { + if (preg_match($pattern, $content)) { + $isPlaceholder = true; + break; + } + } + + // 실제 CERTKEY는 보통 20자 이상의 영문/숫자 조합 + // 설명 텍스트가 아니고, 충분히 긴 경우에만 CERTKEY로 인식 + if (!empty($content) && !$isPlaceholder && strlen($content) >= 10) { + // 추가 검증: 실제 CERTKEY는 보통 영문/숫자/하이픈 조합 + // 설명 텍스트는 한글이나 특수문자가 많음 + $koreanCharCount = preg_match_all('/[가-힣]/u', $content); + $totalCharCount = mb_strlen($content, 'UTF-8'); + + // 한글 비율이 10% 미만이고, 길이가 적절하면 CERTKEY로 인식 + if ($koreanCharCount / max($totalCharCount, 1) < 0.1) { + $barobillCertKey = $content; + } + } +} +if (empty($barobillCertKey) && file_exists($legacyApiKeyFile)) { + $barobillCertKey = trim(file_get_contents($legacyApiKeyFile)); +} + +// 사업자번호 읽기 +$barobillCorpNum = ''; +if (file_exists($corpNumFile)) { + $content = trim(file_get_contents($corpNumFile)); + if (!empty($content) && !preg_match('/^\[여기에/', $content)) { + $barobillCorpNum = str_replace('-', '', $content); + } +} + +// 테스트 모드 확인 +$isTestMode = false; +if (file_exists($testModeFile)) { + $testMode = trim(file_get_contents($testModeFile)); + $isTestMode = (strtolower($testMode) === 'test' || strtolower($testMode) === 'true'); +} + +// 바로빌 사용자 ID (계좌 사용내역 조회에 필요) +// 빈 값이면 전체 계좌 조회, 특정 사용자만 조회하려면 사용자 ID 입력 +$barobillUserIdFile = getenv('DOCUMENT_ROOT') . '/apikey/barobill_user_id.txt'; +$barobillUserId = ''; +if (file_exists($barobillUserIdFile)) { + $content = trim(file_get_contents($barobillUserIdFile)); + if (!empty($content) && !preg_match('/^\[여기에/', $content)) { + $barobillUserId = $content; + } +} + +// 테넌트별 설정 (DB에서 가져오기) +require_once(getenv('DOCUMENT_ROOT') . '/session.php'); +require_once(getenv('DOCUMENT_ROOT') . '/lib/mydb.php'); + +$selectedTenantId = $_SESSION['eaccount_tenant_id'] ?? null; + +// DB에서 테넌트 정보 가져오기 +if ($selectedTenantId) { + try { + $pdo = db_connect(); + if ($pdo) { + $sql = "SELECT id, company_name, corp_num, barobill_user_id + FROM {$DB}.barobill_companies + WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$selectedTenantId]); + $tenant = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($tenant) { + $barobillUserId = $tenant['barobill_user_id']; + $barobillCorpNum = $tenant['corp_num']; + } + } + } catch (Exception $e) { + error_log('테넌트 정보 로드 실패: ' . $e->getMessage()); + } +} else { + // 세션에 테넌트 ID가 없으면 '(주)주일기업'을 기본값으로 찾기 + try { + $pdo = db_connect(); + if ($pdo) { + // '(주)주일기업' 또는 barobill_user_id가 'juil5130'인 회사 찾기 + $sql = "SELECT id, company_name, corp_num, barobill_user_id + FROM {$DB}.barobill_companies + WHERE company_name LIKE '%주일기업%' + OR company_name LIKE '%주일%' + OR barobill_user_id = 'juil5130' + ORDER BY id ASC + LIMIT 1"; + $stmt = $pdo->query($sql); + $tenant = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($tenant) { + $barobillUserId = $tenant['barobill_user_id']; + $barobillCorpNum = $tenant['corp_num']; + $selectedTenantId = $tenant['id']; + // 세션에 저장 + $_SESSION['eaccount_tenant_id'] = $selectedTenantId; + } + } + } catch (Exception $e) { + error_log('기본 테넌트 정보 로드 실패: ' . $e->getMessage()); + } +} + +/** + * 바로빌 사용자 ID 반환 + */ +function getBarobillUserId() { + global $barobillUserId; + return $barobillUserId; +} + +// 바로빌 계좌 SOAP 웹서비스 URL (BANK.asmx) +// 바로빌 계좌 SOAP 웹서비스 URL (BANKACCOUNT.asmx) +$barobillAccountSoapUrl = $isTestMode + ? 'https://testws.baroservice.com/BANKACCOUNT.asmx?WSDL' // 테스트 환경 + : 'https://ws.baroservice.com/BANKACCOUNT.asmx?WSDL'; // 운영 환경 + +// SOAP 클라이언트 초기화 +$barobillAccountSoapClient = null; +$barobillInitError = ''; + +if (!empty($barobillCertKey) || $isTestMode) { + try { + // SSL 검증 비활성화 및 타임아웃 설정 + $context = stream_context_create([ + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true + ] + ]); + + $barobillAccountSoapClient = new SoapClient($barobillAccountSoapUrl, [ + 'trace' => true, + 'encoding' => 'UTF-8', + 'exceptions' => true, + 'connection_timeout' => 30, + 'stream_context' => $context, + 'cache_wsdl' => WSDL_CACHE_NONE // WSDL 캐시 비활성화 + ]); + } catch (Throwable $e) { + $barobillInitError = $e->getMessage(); + error_log('바로빌 계좌 SOAP 클라이언트 생성 실패: ' . $e->getMessage()); + } +} + +/** + * 바로빌 계좌 SOAP 웹서비스 호출 함수 + * + * @param string $method SOAP 메서드명 + * @param array $params SOAP 메서드 파라미터 + * @return array 응답 데이터 + */ +function callBarobillAccountSOAP($method, $params = []) { + global $barobillAccountSoapClient, $barobillCertKey, $barobillCorpNum, $isTestMode, $barobillInitError, $barobillAccountSoapUrl; + + if (!$barobillAccountSoapClient) { + $errorMsg = $isTestMode + ? '바로빌 계좌 SOAP 클라이언트가 초기화되지 않았습니다. (' . ($barobillInitError ?: '알 수 없는 오류') . ')' + : '바로빌 계좌 SOAP 클라이언트가 초기화되지 않았습니다. CERTKEY를 확인하세요. (' . ($barobillInitError ?: '알 수 없는 오류') . ')'; + + return [ + 'success' => false, + 'error' => $errorMsg, + 'error_detail' => [ + 'cert_key_file' => getenv('DOCUMENT_ROOT') . '/apikey/barobill_cert_key.txt', + 'soap_url' => $barobillAccountSoapUrl, + 'init_error' => $barobillInitError, + 'test_mode' => $isTestMode + ] + ]; + } + + if (empty($barobillCertKey) && !$isTestMode) { + return [ + 'success' => false, + 'error' => 'CERTKEY가 설정되지 않았습니다. apikey/barobill_cert_key.txt 파일을 확인하세요.' + ]; + } + + if (empty($barobillCorpNum)) { + return [ + 'success' => false, + 'error' => '사업자번호가 설정되지 않았습니다. apikey/barobill_corp_num.txt 파일을 확인하세요.' + ]; + } + + // CERTKEY와 CorpNum 자동 추가 + // 테스트 모드에서도 CERTKEY가 있으면 사용 (일부 API는 테스트 모드에서도 CERTKEY 필요) + if (!isset($params['CERTKEY'])) { + if ($isTestMode) { + // 테스트 모드: CERTKEY가 있으면 사용, 없으면 빈 값 + // 주의: 일부 API는 테스트 모드에서도 CERTKEY가 필요할 수 있음 + $params['CERTKEY'] = !empty($barobillCertKey) ? $barobillCertKey : ''; + } else { + // 운영 모드: CERTKEY 필수 + $params['CERTKEY'] = $barobillCertKey; + } + } + if (!isset($params['CorpNum'])) { + $params['CorpNum'] = $barobillCorpNum; + } + + try { + error_log('바로빌 계좌 API 호출 - Method: ' . $method . ', CorpNum: ' . $barobillCorpNum); + + // SOAP 요청 로그 수집 (CERTKEY는 마스킹) + $logParams = $params; + if (isset($logParams['CERTKEY'])) { + $logParams['CERTKEY'] = substr($logParams['CERTKEY'], 0, 8) . '...' . substr($logParams['CERTKEY'], -4); + } + + $soapRequest = [ + 'method' => $method, + 'url' => $barobillAccountSoapUrl, + 'params' => $logParams, + 'timestamp' => date('Y-m-d H:i:s') + ]; + + $result = $barobillAccountSoapClient->$method($params); + + // SOAP 요청/응답 XML 로그 수집 + $soapRequestXml = $barobillAccountSoapClient->__getLastRequest(); + $soapResponseXml = $barobillAccountSoapClient->__getLastResponse(); + + $resultProperty = $method . 'Result'; + if (isset($result->$resultProperty)) { + $resultData = $result->$resultProperty; + + // 에러 코드 체크 (음수 값 또는 객체 내부의 음수 값) + $errorCode = null; + + // 직접 숫자로 반환된 경우 + if (is_numeric($resultData) && $resultData < 0) { + $errorCode = $resultData; + } + // 객체 내부에 BankAccountNum이 음수인 경우 (예: -10002) + elseif (is_object($resultData)) { + if (isset($resultData->BankAccountNum) && is_numeric($resultData->BankAccountNum) && $resultData->BankAccountNum < 0) { + $errorCode = $resultData->BankAccountNum; + } + // 다른 필드에서도 음수 값 체크 + foreach (get_object_vars($resultData) as $key => $value) { + if (is_numeric($value) && $value < 0 && ($key == 'CurrentPage' || $key == 'ErrorCode' || $key == 'ResultCode')) { + $errorCode = $value; + break; + } + } + } + + if ($errorCode !== null) { + $errorMsg = '바로빌 계좌 API 오류 코드: ' . $errorCode; + + // 상세 에러 메시지 매핑 + $errorMessages = [ + -10002 => '인증 실패 (-10002). CERTKEY가 올바르지 않거나 만료되었습니다. 바로빌 개발자센터에서 CERTKEY를 확인하세요.', + -50214 => '은행 로그인 실패 (-50214). 바로빌 사이트에서 계좌 비밀번호/인증서를 점검해주세요.', + -24005 => '사용자 정보 불일치 (-24005). 사업자번호를 확인해주세요.', + -25001 => '등록된 계좌가 없습니다 (-25001). 바로빌 사이트에서 계좌를 등록해주세요.', + -25005 => '조회된 데이터가 없습니다 (-25005).', + -25006 => '계좌번호가 잘못되었습니다 (-25006).', + -25007 => '조회 기간이 잘못되었습니다 (-25007).', + ]; + + if (isset($errorMessages[$errorCode])) { + $errorMsg = $errorMessages[$errorCode]; + } + + return [ + 'success' => false, + 'error' => $errorMsg, + 'error_code' => $errorCode, + 'debug' => [ + 'request' => $soapRequest, + 'request_xml' => $soapRequestXml, + 'response_xml' => $soapResponseXml + ] + ]; + } + + return [ + 'success' => true, + 'data' => $resultData, + 'debug' => [ + 'request' => $soapRequest, + 'request_xml' => $soapRequestXml, + 'response_xml' => $soapResponseXml + ] + ]; + } + + return [ + 'success' => true, + 'data' => $result, + 'debug' => [ + 'request' => $soapRequest, + 'request_xml' => $soapRequestXml, + 'response_xml' => $soapResponseXml + ] + ]; + + } catch (SoapFault $e) { + return [ + 'success' => false, + 'error' => 'SOAP 오류: ' . $e->getMessage(), + 'error_code' => $e->getCode() + ]; + } catch (Throwable $e) { + return [ + 'success' => false, + 'error' => 'API 호출 오류 (치명적): ' . $e->getMessage() + ]; + } +} +?> diff --git a/eaccount/api/barobill_card_config.php b/eaccount/api/barobill_card_config.php new file mode 100644 index 0000000..f6801bc --- /dev/null +++ b/eaccount/api/barobill_card_config.php @@ -0,0 +1,404 @@ +load(); + +// 인증서 키(CERTKEY) 파일 경로 +$documentRoot = getenv('DOCUMENT_ROOT'); +$certKeyFile = $documentRoot . '/apikey/barobill_cert_key.txt'; +$legacyApiKeyFile = $documentRoot . '/apikey/barobill_api_key.txt'; +$corpNumFile = $documentRoot . '/apikey/barobill_corp_num.txt'; +$testModeFile = $documentRoot . '/apikey/barobill_test_mode.txt'; + +// CERTKEY 읽기 +$barobillCertKey = ''; +if (file_exists($certKeyFile)) { + $content = trim(file_get_contents($certKeyFile)); + // 설명 텍스트가 아닌 실제 키만 추출 (대괄호 안의 내용 제외, =로 시작하는 경우 제외) + if (!empty($content) && !preg_match('/^\[여기에/', $content) && !preg_match('/^=/', $content) && strpos($content, '바로빌 CERTKEY') === false) { + $barobillCertKey = $content; + } +} +if (empty($barobillCertKey) && file_exists($legacyApiKeyFile)) { + $barobillCertKey = trim(file_get_contents($legacyApiKeyFile)); +} + +// 사업자번호 읽기 +$barobillCorpNum = ''; +if (file_exists($corpNumFile)) { + $content = trim(file_get_contents($corpNumFile)); + if (!empty($content) && !preg_match('/^\[여기에/', $content)) { + $barobillCorpNum = str_replace('-', '', $content); + } +} + +// 테스트 모드 확인 +$isTestMode = false; +if (file_exists($testModeFile)) { + $testMode = trim(file_get_contents($testModeFile)); + $isTestMode = (strtolower($testMode) === 'test' || strtolower($testMode) === 'true'); +} + +// 바로빌 사용자 ID (카드 사용내역 조회에 필요) +// 빈 값이면 전체 카드 조회, 특정 사용자만 조회하려면 사용자 ID 입력 +$barobillUserIdFile = getenv('DOCUMENT_ROOT') . '/apikey/barobill_user_id.txt'; +$barobillUserId = ''; +if (file_exists($barobillUserIdFile)) { + $content = trim(file_get_contents($barobillUserIdFile)); + if (!empty($content) && !preg_match('/^\[여기에/', $content)) { + $barobillUserId = $content; + } +} + +/** + * 바로빌 사용자 ID 반환 + */ +function getBarobillUserId() { + global $barobillUserId; + return $barobillUserId; +} + +// 바로빌 카드 SOAP 웹서비스 URL +$barobillCardSoapUrl = $isTestMode + ? 'https://testws.baroservice.com/CARD.asmx?WSDL' // 테스트 환경 + : 'https://ws.baroservice.com/CARD.asmx?WSDL'; // 운영 환경 + +// SOAP 클라이언트 초기화 +$barobillCardSoapClient = null; +if (!empty($barobillCertKey) || $isTestMode) { + try { + $barobillCardSoapClient = new SoapClient($barobillCardSoapUrl, [ + 'trace' => true, + 'encoding' => 'UTF-8', + 'exceptions' => true, + 'connection_timeout' => 30 + ]); + } catch (Throwable $e) { + error_log('바로빌 카드 SOAP 클라이언트 생성 실패: ' . $e->getMessage()); + } +} + +/** + * 바로빌 카드 SOAP 웹서비스 호출 함수 + * + * @param string $method SOAP 메서드명 + * @param array $params SOAP 메서드 파라미터 + * @return array 응답 데이터 + */ +function callBarobillCardSOAP($method, $params = []) { + global $barobillCardSoapClient, $barobillCertKey, $barobillCorpNum, $isTestMode; + + if (!$barobillCardSoapClient) { + return [ + 'success' => false, + 'error' => '바로빌 카드 SOAP 클라이언트가 초기화되지 않았습니다. CERTKEY를 확인하세요.', + 'error_detail' => [ + 'cert_key_file' => getenv('DOCUMENT_ROOT') . '/apikey/barobill_cert_key.txt', + 'soap_url' => $isTestMode ? 'https://testws.baroservice.com/CARD.asmx?WSDL' : 'https://ws.baroservice.com/CARD.asmx?WSDL' + ] + ]; + } + + if (empty($barobillCertKey) && !$isTestMode) { + return [ + 'success' => false, + 'error' => 'CERTKEY가 설정되지 않았습니다. apikey/barobill_cert_key.txt 파일을 확인하세요.' + ]; + } + + if (empty($barobillCorpNum)) { + return [ + 'success' => false, + 'error' => '사업자번호가 설정되지 않았습니다. apikey/barobill_corp_num.txt 파일을 확인하세요.' + ]; + } + + // CERTKEY와 CorpNum 자동 추가 + if (!isset($params['CERTKEY'])) { + $params['CERTKEY'] = $barobillCertKey; + } + if (!isset($params['CorpNum'])) { + $params['CorpNum'] = $barobillCorpNum; + } + + try { + error_log('바로빌 카드 API 호출 - Method: ' . $method . ', CorpNum: ' . $barobillCorpNum); + + $result = $barobillCardSoapClient->$method($params); + + $resultProperty = $method . 'Result'; + if (isset($result->$resultProperty)) { + $resultData = $result->$resultProperty; + + // 에러 코드 체크 (음수 값) + if (is_numeric($resultData) && $resultData < 0) { + return [ + 'success' => false, + 'error' => '바로빌 카드 API 오류 코드: ' . $resultData, + 'error_code' => $resultData + ]; + } + + return [ + 'success' => true, + 'data' => $resultData + ]; + } + + return [ + 'success' => true, + 'data' => $result + ]; + + } catch (SoapFault $e) { + return [ + 'success' => false, + 'error' => 'SOAP 오류: ' . $e->getMessage(), + 'error_code' => $e->getCode() + ]; + } catch (Throwable $e) { + return [ + 'success' => false, + 'error' => 'API 호출 오류 (치명적): ' . $e->getMessage() + ]; + } +} + +/** + * 등록된 카드 목록 조회 (GetCardEx2 API 사용) + * API 레퍼런스: https://dev.barobill.co.kr/docs/references/카드조회-API#GetCardEx2 + * + * @param int $availOnly 0: 전체, 1: 사용가능한 카드만 + * @return array 카드 목록 + */ +function getCardList($availOnly = 0) { + $result = callBarobillCardSOAP('GetCardEx2', [ + 'AvailOnly' => $availOnly + ]); + + if (!$result['success']) { + return $result; + } + + $cards = []; + $data = $result['data']; + + // GetCardEx2는 CardEx 배열을 반환 + if (!isset($data->CardEx)) { + return ['success' => true, 'data' => []]; + } + + if (!is_array($data->CardEx)) { + $cards = [$data->CardEx]; + } else { + $cards = $data->CardEx; + } + + // 에러 체크: CardNum이 음수면 에러 코드 + if (count($cards) == 1 && isset($cards[0]->CardNum) && $cards[0]->CardNum < 0) { + return [ + 'success' => false, + 'error' => '카드 목록 조회 실패', + 'error_code' => $cards[0]->CardNum + ]; + } + + return ['success' => true, 'data' => $cards]; +} + +/** + * 기간별 카드 사용내역 조회 + * + * @param string $cardNum 카드번호 (빈값이면 전체) + * @param string $startDate 시작일 (YYYYMMDD) + * @param string $endDate 종료일 (YYYYMMDD) + * @param int $countPerPage 페이지당 건수 + * @param int $currentPage 현재 페이지 + * @param int $orderDirection 정렬 (1: 오름차순, 2: 내림차순) + * @param string $userId 바로빌 사용자 ID (빈값이면 전체) + * @return array 사용내역 + */ +function getPeriodCardUsage($cardNum = '', $startDate = '', $endDate = '', $countPerPage = 50, $currentPage = 1, $orderDirection = 2, $userId = '') { + global $barobillCorpNum; + + // 바로빌 사용자 ID 파일에서 읽기 (없으면 빈값) + $barobillUserId = getBarobillUserId(); + if (!empty($userId)) { + $barobillUserId = $userId; + } + + $result = callBarobillCardSOAP('GetPeriodCardApprovalLog', [ + 'ID' => $barobillUserId, + 'CardNum' => $cardNum, + 'StartDate' => $startDate, + 'EndDate' => $endDate, + 'CountPerPage' => $countPerPage, + 'CurrentPage' => $currentPage, + 'OrderDirection' => $orderDirection + ]); + + if (!$result['success']) { + return $result; + } + + return parseCardUsageResult($result['data']); +} + +/** + * 일별 카드 사용내역 조회 + * + * @param string $cardNum 카드번호 (빈값이면 전체) + * @param string $baseDate 기준일 (YYYYMMDD) + * @param int $countPerPage 페이지당 건수 + * @param int $currentPage 현재 페이지 + * @param int $orderDirection 정렬 (1: 오름차순, 2: 내림차순) + * @param string $userId 바로빌 사용자 ID (빈값이면 전체) + * @return array 사용내역 + */ +function getDailyCardUsage($cardNum = '', $baseDate = '', $countPerPage = 50, $currentPage = 1, $orderDirection = 2, $userId = '') { + $barobillUserId = getBarobillUserId(); + if (!empty($userId)) { + $barobillUserId = $userId; + } + + $result = callBarobillCardSOAP('GetDailyCardApprovalLog', [ + 'ID' => $barobillUserId, + 'CardNum' => $cardNum, + 'BaseDate' => $baseDate, + 'CountPerPage' => $countPerPage, + 'CurrentPage' => $currentPage, + 'OrderDirection' => $orderDirection + ]); + + if (!$result['success']) { + return $result; + } + + return parseCardUsageResult($result['data']); +} + +/** + * 월별 카드 사용내역 조회 + * + * @param string $cardNum 카드번호 (빈값이면 전체) + * @param string $baseMonth 기준월 (YYYYMM) + * @param int $countPerPage 페이지당 건수 + * @param int $currentPage 현재 페이지 + * @param int $orderDirection 정렬 (1: 오름차순, 2: 내림차순) + * @param string $userId 바로빌 사용자 ID (빈값이면 전체) + * @return array 사용내역 + */ +function getMonthlyCardUsage($cardNum = '', $baseMonth = '', $countPerPage = 50, $currentPage = 1, $orderDirection = 2, $userId = '') { + $barobillUserId = getBarobillUserId(); + if (!empty($userId)) { + $barobillUserId = $userId; + } + + $result = callBarobillCardSOAP('GetMonthlyCardApprovalLog', [ + 'ID' => $barobillUserId, + 'CardNum' => $cardNum, + 'BaseMonth' => $baseMonth, + 'CountPerPage' => $countPerPage, + 'CurrentPage' => $currentPage, + 'OrderDirection' => $orderDirection + ]); + + if (!$result['success']) { + return $result; + } + + return parseCardUsageResult($result['data']); +} + +/** + * 카드 사용내역 결과 파싱 + * + * @param object $data SOAP 응답 데이터 + * @return array 파싱된 결과 + */ +function parseCardUsageResult($data) { + // 에러 체크 + if (isset($data->CurrentPage) && $data->CurrentPage < 0) { + $errorCode = $data->CurrentPage; + + // -24005: 조회 데이터 없음 (정상 케이스로 처리) + // -24001: 등록된 카드 없음 + // -24002: 조회 기간 오류 + if ($errorCode == -24005 || $errorCode == -24001) { + // 데이터 없음 - 빈 배열 반환 (에러가 아님) + return [ + 'success' => true, + 'data' => [ + 'currentPage' => 1, + 'countPerPage' => 50, + 'maxPageNum' => 1, + 'maxIndex' => 0, + 'logs' => [] + ] + ]; + } + + return [ + 'success' => false, + 'error' => '카드 사용내역 조회 실패', + 'error_code' => $errorCode + ]; + } + + $logs = []; + if (isset($data->CardLogList) && isset($data->CardLogList->CardApprovalLog)) { + if (!is_array($data->CardLogList->CardApprovalLog)) { + $logs = [$data->CardLogList->CardApprovalLog]; + } else { + $logs = $data->CardLogList->CardApprovalLog; + } + } + + return [ + 'success' => true, + 'data' => [ + 'currentPage' => $data->CurrentPage ?? 1, + 'countPerPage' => $data->CountPerPage ?? 50, + 'maxPageNum' => $data->MaxPageNum ?? 1, + 'maxIndex' => $data->MaxIndex ?? 0, + 'logs' => $logs + ] + ]; +} + +/** + * 카드 등록 + * + * @param array $cardData 카드 데이터 + * @return array 응답 데이터 + */ +function registerCard($cardData) { + return callBarobillCardSOAP('RegistCardEx', [ + 'CollectCycle' => $cardData['collectCycle'] ?? '1', // 수집주기 (1: 1일 1회) + 'CardCompany' => $cardData['cardCompany'] ?? '', // 카드사 코드 + 'CardType' => $cardData['cardType'] ?? '1', // 카드 종류 (1: 개인, 2: 법인) + 'CardNum' => $cardData['cardNum'] ?? '', // 카드번호 + 'WebId' => $cardData['webId'] ?? '', // 카드사 웹 ID + 'WebPwd' => $cardData['webPwd'] ?? '', // 카드사 웹 비밀번호 + 'Alias' => $cardData['alias'] ?? '', // 카드 별칭 + 'Usage' => $cardData['usage'] ?? '1' // 용도 (1: 세금계산서, 2: 기타) + ]); +} + +?> + diff --git a/eaccount/api/cards.php b/eaccount/api/cards.php new file mode 100644 index 0000000..51dc538 --- /dev/null +++ b/eaccount/api/cards.php @@ -0,0 +1,225 @@ +CardCompanyCode ?? $card->CardCompany ?? ''; + + // 카드 브랜드 (비자, 마스터카드 등) 추측 + $cardBrand = guessCardTypeFromNumber($card->CardNum ?? ''); + + // 카드 회사명 (신한, KB 등) + $cardCompanyName = !empty($card->CardCompanyName) + ? $card->CardCompanyName + : getCardCompanyName($cardCompanyCode); + + $cards[] = [ + 'cardNum' => $card->CardNum ?? '', + 'cardNumMasked' => maskCardNumber($card->CardNum ?? ''), + 'cardCompany' => $cardCompanyCode, + 'cardCompanyName' => $cardCompanyName, + 'cardBrand' => $cardBrand, // 카드 브랜드 (비자, 마스터카드 등) + 'alias' => $card->Alias ?? '', + 'cardType' => $card->CardType ?? '', + 'cardTypeName' => getCardTypeName($card->CardType ?? ''), + 'status' => $card->Status ?? '', + 'statusName' => getCardStatusName($card->Status ?? ''), + 'collectCycle' => $card->CollectCycle ?? '', + 'collectCycleName' => getCollectCycleName($card->CollectCycle ?? ''), + 'lastCollectDate' => formatDate($card->LastCollectDate ?? ''), + 'lastCollectResult' => $card->LastCollectResult ?? '', + 'lastCollectResultName' => getCollectResultName($card->LastCollectResult ?? ''), + 'nextExtendDate' => formatDate($card->NextExtendDate ?? ''), + 'registDate' => formatDate($card->RegistDate ?? ''), + 'webId' => $card->WebId ?? '' + ]; + } + + echo json_encode([ + 'success' => true, + 'cards' => $cards, + 'count' => count($cards) + ], JSON_UNESCAPED_UNICODE); + } else { + echo json_encode([ + 'success' => false, + 'error' => $result['error'], + 'error_code' => $result['error_code'] ?? null + ], JSON_UNESCAPED_UNICODE); + } +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} + +/** + * 카드번호 마스킹 + */ +function maskCardNumber($cardNum) { + if (strlen($cardNum) < 8) return $cardNum; + return substr($cardNum, 0, 4) . '-****-****-' . substr($cardNum, -4); +} + +/** + * 날짜 포맷팅 + */ +function formatDate($date) { + if (empty($date)) return ''; + if (strlen($date) === 8) { + return substr($date, 0, 4) . '-' . substr($date, 4, 2) . '-' . substr($date, 6, 2); + } + return $date; +} + +/** + * 카드번호로 카드 종류 추측 (BIN 코드 기반) + */ +function guessCardTypeFromNumber($cardNum) { + if (empty($cardNum) || strlen($cardNum) < 4) { + return '카드'; + } + + $bin = substr($cardNum, 0, 4); + + // 주요 카드사 BIN 코드 + $binMappings = [ + '4518' => '비자', + '4092' => '비자', + '4569' => '비자', + '4563' => '비자', + '5' => '마스터카드', // 5로 시작 + '3528' => 'JCB', + '3529' => 'JCB', + '3' => '아멕스/다이너스', // 34, 37로 시작 + '9' => '국내전용카드' + ]; + + // 정확한 매칭 시도 + if (isset($binMappings[$bin])) { + return $binMappings[$bin]; + } + + // 첫 번째 숫자로 매칭 시도 + $firstDigit = substr($cardNum, 0, 1); + if (isset($binMappings[$firstDigit])) { + return $binMappings[$firstDigit]; + } + + return '카드'; +} + +/** + * 카드사 코드 -> 이름 변환 + * 바로빌 카드사 코드 참고 + */ +function getCardCompanyName($code) { + $companies = [ + '01' => '비씨카드', + '02' => 'KB국민카드', + '03' => '하나카드(외환)', + '04' => '삼성카드', + '06' => '신한카드', + '07' => '현대카드', + '08' => '롯데카드', + '11' => 'NH농협카드', + '12' => '수협카드', + '13' => '씨티카드', + '14' => '우리카드', + '15' => '광주카드', + '16' => '전북카드', + '21' => '하나카드', + '22' => '제주카드', + '23' => 'SC제일카드', + '25' => 'KDB산업카드', + '26' => 'IBK기업카드', + '27' => '새마을금고', + '28' => '신협카드', + '29' => '저축은행', + '30' => '우체국카드', + '31' => '카카오뱅크', + '32' => 'K뱅크', + '33' => '토스뱅크', + 'BC' => '비씨카드', + 'KB' => 'KB국민카드', + 'HANA' => '하나카드', + 'SAMSUNG' => '삼성카드', + 'SHINHAN' => '신한카드', + 'HYUNDAI' => '현대카드', + 'LOTTE' => '롯데카드', + 'NH' => 'NH농협카드', + 'SUHYUP' => '수협카드', + 'CITI' => '씨티카드', + 'WOORI' => '우리카드', + 'KJBANK' => '광주카드', + 'JBBANK' => '전북카드' + ]; + return $companies[$code] ?? $code; +} + +/** + * 카드 종류 코드 -> 이름 변환 + */ +function getCardTypeName($type) { + $types = [ + '1' => '개인카드', + '2' => '법인카드' + ]; + return $types[$type] ?? $type; +} + +/** + * 카드 상태 코드 -> 이름 변환 + */ +function getCardStatusName($status) { + $statuses = [ + '0' => '대기중', + '1' => '정상', + '2' => '해지', + '3' => '수집오류', + '4' => '일시중지' + ]; + return $statuses[$status] ?? $status; +} + +/** + * 수집주기 코드 -> 이름 변환 + */ +function getCollectCycleName($cycle) { + $cycles = [ + '1' => '1일 1회', + '2' => '1일 2회', + '3' => '1일 3회' + ]; + return $cycles[$cycle] ?? $cycle; +} + +/** + * 수집결과 코드 -> 이름 변환 + */ +function getCollectResultName($result) { + $results = [ + '0' => '대기', + '1' => '성공', + '2' => '실패', + '3' => '진행중' + ]; + return $results[$result] ?? $result; +} +?> + diff --git a/eaccount/api/check_api_config.php b/eaccount/api/check_api_config.php new file mode 100644 index 0000000..97f7071 --- /dev/null +++ b/eaccount/api/check_api_config.php @@ -0,0 +1,138 @@ + [ + 'file_exists' => file_exists($documentRoot . '/apikey/barobill_cert_key.txt'), + 'file_path' => $documentRoot . '/apikey/barobill_cert_key.txt', + 'is_set' => !empty($barobillCertKey), + 'length' => strlen($barobillCertKey), + 'preview' => !empty($barobillCertKey) ? substr($barobillCertKey, 0, 8) . '...' . substr($barobillCertKey, -4) : 'NOT SET', + 'raw_content' => file_exists($documentRoot . '/apikey/barobill_cert_key.txt') + ? file_get_contents($documentRoot . '/apikey/barobill_cert_key.txt') + : 'FILE NOT FOUND', + 'is_placeholder' => !empty($barobillCertKey) ? false : ( + file_exists($documentRoot . '/apikey/barobill_cert_key.txt') + ? (strpos(file_get_contents($documentRoot . '/apikey/barobill_cert_key.txt'), '[여기에') !== false + || strpos(file_get_contents($documentRoot . '/apikey/barobill_cert_key.txt'), '바로빌 CERTKEY') !== false + || strpos(file_get_contents($documentRoot . '/apikey/barobill_cert_key.txt'), '================================') !== false) + : false + ) + ], + 'corp_num' => [ + 'file_exists' => file_exists($documentRoot . '/apikey/barobill_corp_num.txt'), + 'file_path' => $documentRoot . '/apikey/barobill_corp_num.txt', + 'is_set' => !empty($barobillCorpNum), + 'value' => $barobillCorpNum, + 'raw_content' => file_exists($documentRoot . '/apikey/barobill_corp_num.txt') + ? file_get_contents($documentRoot . '/apikey/barobill_corp_num.txt') + : 'FILE NOT FOUND' + ], + 'user_id' => [ + 'file_exists' => file_exists($documentRoot . '/apikey/barobill_user_id.txt'), + 'file_path' => $documentRoot . '/apikey/barobill_user_id.txt', + 'is_set' => !empty($barobillUserId), + 'value' => $barobillUserId, + 'raw_content' => file_exists($documentRoot . '/apikey/barobill_user_id.txt') + ? file_get_contents($documentRoot . '/apikey/barobill_user_id.txt') + : 'FILE NOT FOUND' + ], + 'test_mode' => [ + 'file_exists' => file_exists($documentRoot . '/apikey/barobill_test_mode.txt'), + 'is_active' => $isTestMode, + 'raw_content' => file_exists($documentRoot . '/apikey/barobill_test_mode.txt') + ? file_get_contents($documentRoot . '/apikey/barobill_test_mode.txt') + : 'FILE NOT FOUND' + ], + 'soap_client' => [ + 'url' => $barobillAccountSoapUrl, + 'initialized' => isset($barobillAccountSoapClient) && $barobillAccountSoapClient !== null + ], + 'hardcoded_values' => [ + 'note' => '⚠️ barobill_account_config.php에 하드코딩된 값이 있을 수 있습니다.', + 'check_file' => 'eaccount/api/barobill_account_config.php (62-68줄)' + ] +]; + +// 실제 API 호출 테스트 +// 테스트 모드일 때는 CERTKEY가 없어도 테스트 가능 +$testResult = null; +$canTest = false; +if ($isTestMode) { + // 테스트 모드: CERTKEY 불필요, 사업자번호만 확인 + $canTest = !empty($barobillCorpNum); +} else { + // 운영 모드: CERTKEY와 사업자번호 모두 필요 + $canTest = !empty($barobillCertKey) && !empty($barobillCorpNum); +} + +if ($canTest) { + try { + $testResult = callBarobillAccountSOAP('GetBankAccountEx', [ + 'AvailOnly' => 0 + ]); + + $diagnostics['api_test'] = [ + 'success' => $testResult['success'], + 'error' => $testResult['error'] ?? null, + 'error_code' => $testResult['error_code'] ?? null, + 'has_data' => isset($testResult['data']), + 'debug_available' => isset($testResult['debug']) + ]; + + if (isset($testResult['debug'])) { + $diagnostics['api_test']['request_preview'] = $testResult['debug']['request'] ?? null; + } + } catch (Exception $e) { + $diagnostics['api_test'] = [ + 'success' => false, + 'error' => 'Exception: ' . $e->getMessage() + ]; + } +} else { + if ($isTestMode) { + $diagnostics['api_test'] = [ + 'skipped' => true, + 'reason' => '테스트 모드: 사업자번호가 설정되지 않았습니다. (CERTKEY는 불필요)' + ]; + } else { + $diagnostics['api_test'] = [ + 'skipped' => true, + 'reason' => '운영 모드: CERTKEY 또는 사업자번호가 설정되지 않았습니다.' + ]; + } +} + +echo json_encode([ + 'success' => true, + 'diagnostics' => $diagnostics, + 'recommendations' => array_filter([ + !$isTestMode && !$diagnostics['cert_key']['is_set'] ? 'CERTKEY를 설정하세요: apikey/barobill_cert_key.txt 파일에 바로빌 CERTKEY를 입력하세요.' : null, + !$isTestMode && isset($diagnostics['cert_key']['is_placeholder']) && $diagnostics['cert_key']['is_placeholder'] ? '⚠️ CERTKEY 파일에 설명 텍스트만 있습니다. 파일의 모든 설명을 삭제하고 실제 CERTKEY 값만 입력하세요. (예: "2DD6C76C-1234-5678-ABCD-EF1234561826")' : null, + !$diagnostics['corp_num']['is_set'] ? '사업자번호를 설정하세요: apikey/barobill_corp_num.txt 파일에 사업자번호를 입력하세요.' : null, + !$isTestMode && $diagnostics['cert_key']['file_exists'] && empty(trim($diagnostics['cert_key']['raw_content'])) ? 'CERTKEY 파일이 비어있습니다. 바로빌 사이트에서 CERTKEY를 확인하고 입력하세요.' : null, + !$isTestMode && strpos($diagnostics['cert_key']['raw_content'], '[여기에') !== false ? 'CERTKEY 파일에 설명 텍스트가 남아있습니다. 실제 CERTKEY 값만 입력하세요.' : null, + isset($diagnostics['api_test']['error_code']) && $diagnostics['api_test']['error_code'] == -24005 ? '사업자번호가 잘못되었습니다. 바로빌 사이트에 로그인하여 등록된 사업자번호를 확인하세요.' : null, + isset($diagnostics['api_test']['error_code']) && $diagnostics['api_test']['error_code'] == -25001 ? '등록된 계좌가 없습니다. 바로빌 사이트에서 계좌를 먼저 등록하세요.' : null, + $isTestMode ? '현재 테스트 모드입니다. CERTKEY는 필요하지 않습니다.' : null, + ]), + 'next_steps' => [ + '1. 바로빌 사이트(https://www.barobill.co.kr)에 로그인', + '2. 마이페이지 > API 설정에서 CERTKEY 확인', + '3. apikey/barobill_cert_key.txt 파일에 CERTKEY 입력 (설명 텍스트 제외)', + '4. apikey/barobill_corp_num.txt 파일에 사업자번호 입력 (하이픈 제외 가능)', + '5. (주)코드브릿지 산하 기업인 경우, barobill_account_config.php의 하드코딩된 값 확인 필요' + ] +], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); +?> + diff --git a/eaccount/api/debug_accounts.php b/eaccount/api/debug_accounts.php new file mode 100644 index 0000000..54719b0 --- /dev/null +++ b/eaccount/api/debug_accounts.php @@ -0,0 +1,273 @@ +load(); + +require_once('barobill_account_config.php'); +require_once(getenv('DOCUMENT_ROOT') . '/session.php'); +require_once(getenv('DOCUMENT_ROOT') . '/lib/mydb.php'); + +$debug = [ + 'step' => [], + 'tenant_info' => [], + 'local_db' => [], + 'barobill_api' => [], + 'final_result' => [] +]; + +try { + // Step 1: 세션에서 테넌트 ID 확인 + $selectedTenantId = $_SESSION['eaccount_tenant_id'] ?? null; + $debug['step'][] = '1. 세션에서 테넌트 ID 확인'; + $debug['tenant_info']['session_tenant_id'] = $selectedTenantId; + + // Step 2: DB에서 테넌트 정보 확인 + $pdo = db_connect(); + if (!$pdo) { + throw new Exception("Database connection failed."); + } + + $debug['step'][] = '2. DB 연결 성공'; + + // 테넌트 정보 조회 + if ($selectedTenantId) { + $sql = "SELECT id, company_name, corp_num, barobill_user_id + FROM {$DB}.barobill_companies + WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$selectedTenantId]); + $tenant = $stmt->fetch(PDO::FETCH_ASSOC); + + $debug['tenant_info']['db_query'] = $sql; + $debug['tenant_info']['db_params'] = [$selectedTenantId]; + $debug['tenant_info']['tenant_found'] = $tenant ? true : false; + $debug['tenant_info']['tenant_data'] = $tenant; + + if ($tenant) { + $debug['step'][] = '3. 테넌트 정보 조회 성공: ' . $tenant['company_name']; + } else { + $debug['step'][] = '3. 테넌트 정보 조회 실패: ID ' . $selectedTenantId . '를 찾을 수 없음'; + } + } else { + // 기본값으로 주일기업 찾기 + $sql = "SELECT id, company_name, corp_num, barobill_user_id + FROM {$DB}.barobill_companies + WHERE company_name LIKE '%주일기업%' + OR company_name LIKE '%주일%' + OR barobill_user_id = 'juil5130' + ORDER BY id ASC + LIMIT 1"; + $stmt = $pdo->query($sql); + $tenant = $stmt->fetch(PDO::FETCH_ASSOC); + + $debug['tenant_info']['default_search_query'] = $sql; + $debug['tenant_info']['tenant_found'] = $tenant ? true : false; + $debug['tenant_info']['tenant_data'] = $tenant; + + if ($tenant) { + $selectedTenantId = $tenant['id']; + $debug['step'][] = '3. 기본 테넌트(주일기업) 찾기 성공: ' . $tenant['company_name']; + } else { + $debug['step'][] = '3. 기본 테넌트(주일기업) 찾기 실패'; + } + } + + // Step 3: 로컬 DB에서 계좌 정보 조회 + if ($selectedTenantId) { + $debug['step'][] = '4. 로컬 DB 계좌 정보 조회 시작'; + + $accountSql = "SELECT id, company_id, bank_code, account_num, account_pwd + FROM {$DB}.company_accounts + WHERE company_id = ? + ORDER BY id DESC"; + $accountStmt = $pdo->prepare($accountSql); + $accountStmt->execute([$selectedTenantId]); + $localAccounts = $accountStmt->fetchAll(PDO::FETCH_ASSOC); + + $debug['local_db']['query'] = $accountSql; + $debug['local_db']['params'] = [$selectedTenantId]; + $debug['local_db']['count'] = count($localAccounts); + $debug['local_db']['accounts'] = $localAccounts; + $debug['step'][] = '4. 로컬 DB 계좌 정보 조회 완료: ' . count($localAccounts) . '개'; + + // 모든 회사의 계좌 정보도 확인 (디버깅용) + $allAccountsSql = "SELECT ca.*, c.company_name, c.barobill_user_id + FROM {$DB}.company_accounts ca + LEFT JOIN {$DB}.barobill_companies c ON ca.company_id = c.id + ORDER BY ca.id DESC + LIMIT 20"; + $allAccountsStmt = $pdo->query($allAccountsSql); + $allAccounts = $allAccountsStmt->fetchAll(PDO::FETCH_ASSOC); + + $debug['local_db']['all_companies_accounts'] = $allAccounts; + $debug['local_db']['all_companies_accounts_count'] = count($allAccounts); + } else { + $debug['local_db']['error'] = '테넌트 ID가 없어서 로컬 DB 조회 불가'; + } + + // Step 4: 바로빌 API 설정 확인 + global $barobillUserId, $barobillCorpNum, $barobillCertKey, $isTestMode; + $debug['barobill_api']['config'] = [ + 'user_id' => $barobillUserId, + 'corp_num' => $barobillCorpNum, + 'cert_key_length' => strlen($barobillCertKey), + 'cert_key_preview' => !empty($barobillCertKey) ? substr($barobillCertKey, 0, 8) . '...' . substr($barobillCertKey, -4) : 'NOT SET', + 'test_mode' => $isTestMode + ]; + $debug['step'][] = '5. 바로빌 API 설정 확인 완료'; + + // Step 5: 바로빌 API 호출 시도 + $debug['step'][] = '6. 바로빌 API 호출 시작'; + $result = callBarobillAccountSOAP('GetBankAccountEx', [ + 'AvailOnly' => 0 + ]); + + $debug['barobill_api']['success'] = $result['success']; + $debug['barobill_api']['error'] = $result['error'] ?? null; + $debug['barobill_api']['error_code'] = $result['error_code'] ?? null; + + if ($result['success']) { + $data = $result['data']; + $accountList = []; + + if (isset($data->BankAccountEx)) { + if (is_array($data->BankAccountEx)) { + $accountList = $data->BankAccountEx; + } else { + $accountList = [$data->BankAccountEx]; + } + } + + $barobillAccounts = []; + foreach ($accountList as $acc) { + if (isset($acc->BankAccountNum) && is_numeric($acc->BankAccountNum) && $acc->BankAccountNum < 0) { + continue; + } + + $barobillAccounts[] = [ + 'bankAccountNum' => $acc->BankAccountNum ?? '', + 'bankCode' => $acc->BankCode ?? '', + 'bankName' => getBankName($acc->BankCode ?? ''), + 'accountName' => $acc->AccountName ?? '', + ]; + } + + $debug['barobill_api']['accounts'] = $barobillAccounts; + $debug['barobill_api']['count'] = count($barobillAccounts); + $debug['step'][] = '6. 바로빌 API 호출 성공: ' . count($barobillAccounts) . '개 계좌'; + } else { + $debug['barobill_api']['accounts'] = []; + $debug['barobill_api']['count'] = 0; + $debug['step'][] = '6. 바로빌 API 호출 실패: ' . ($result['error'] ?? '알 수 없는 오류'); + } + + // Step 6: 최종 결과 통합 + $allAccounts = []; + + // 로컬 DB 계좌 추가 + if (isset($localAccounts)) { + foreach ($localAccounts as $localAcc) { + $allAccounts[] = [ + 'source' => 'local_db', + 'bankAccountNum' => $localAcc['account_num'], + 'bankCode' => $localAcc['bank_code'], + 'bankName' => getBankName($localAcc['bank_code']), + ]; + } + } + + // 바로빌 API 계좌 추가 + if (isset($barobillAccounts)) { + foreach ($barobillAccounts as $barobillAcc) { + $exists = false; + foreach ($allAccounts as &$existingAcc) { + if ($existingAcc['bankAccountNum'] === $barobillAcc['bankAccountNum'] && + $existingAcc['source'] === 'local_db') { + $existingAcc['source'] = 'both'; + $exists = true; + break; + } + } + if (!$exists) { + $allAccounts[] = array_merge($barobillAcc, ['source' => 'barobill_api']); + } + } + } + + $debug['final_result']['total_count'] = count($allAccounts); + $debug['final_result']['accounts'] = $allAccounts; + $debug['step'][] = '7. 최종 통합 완료: ' . count($allAccounts) . '개 계좌'; + + // 모든 barobill_companies 목록도 확인 + $allCompaniesSql = "SELECT id, company_name, corp_num, barobill_user_id, parent_id + FROM {$DB}.barobill_companies + ORDER BY id ASC"; + $allCompaniesStmt = $pdo->query($allCompaniesSql); + $allCompanies = $allCompaniesStmt->fetchAll(PDO::FETCH_ASSOC); + + $debug['tenant_info']['all_companies'] = $allCompanies; + $debug['tenant_info']['all_companies_count'] = count($allCompanies); + + echo json_encode([ + 'success' => true, + 'debug' => $debug, + 'summary' => [ + 'tenant_id' => $selectedTenantId, + 'tenant_name' => $tenant['company_name'] ?? '알 수 없음', + 'local_accounts_count' => count($localAccounts ?? []), + 'barobill_accounts_count' => count($barobillAccounts ?? []), + 'total_accounts_count' => count($allAccounts) + ] + ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + +} catch (Exception $e) { + $debug['error'] = $e->getMessage(); + $debug['error_trace'] = $e->getTraceAsString(); + + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage(), + 'debug' => $debug + ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); +} + +/** + * 은행 코드 -> 은행명 변환 + */ +function getBankName($code) { + $banks = [ + '002' => 'KDB산업은행', + '003' => 'IBK기업은행', + '004' => 'KB국민은행', + '007' => '수협은행', + '011' => 'NH농협은행', + '012' => '지역농축협', + '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; +} +?> + diff --git a/eaccount/api/get_tenants.php b/eaccount/api/get_tenants.php new file mode 100644 index 0000000..1123c0f --- /dev/null +++ b/eaccount/api/get_tenants.php @@ -0,0 +1,97 @@ +load(); + +require_once(getenv('DOCUMENT_ROOT') . '/session.php'); +require_once(getenv('DOCUMENT_ROOT') . '/lib/mydb.php'); + +try { + $pdo = db_connect(); + + if (!$pdo) { + throw new Exception("Database connection failed."); + } + + // barobill_companies 테이블에서 모든 회사 가져오기 + $sql = "SELECT c.*, p.company_name as parent_name, p.barobill_user_id as parent_user_id + FROM {$DB}.barobill_companies c + LEFT JOIN {$DB}.barobill_companies p ON c.parent_id = p.id + ORDER BY c.parent_id ASC, c.id ASC"; + $stmt = $pdo->query($sql); + $companies = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // 계좌 정보 확인 (company_accounts 테이블에서) + $tenants = []; + foreach ($companies as $company) { + // 계좌 정보 확인 + $accountSql = "SELECT COUNT(*) as count FROM {$DB}.company_accounts WHERE company_id = ?"; + $accountStmt = $pdo->prepare($accountSql); + $accountStmt->execute([$company['id']]); + $accountResult = $accountStmt->fetch(PDO::FETCH_ASSOC); + $hasAccount = ($accountResult['count'] > 0); + + $tenants[] = [ + 'id' => $company['id'], + 'name' => $company['company_name'], + 'corp_num' => $company['corp_num'], + 'user_id' => $company['barobill_user_id'], + 'parent_id' => $company['parent_id'], + 'parent_name' => $company['parent_name'] ?? null, + 'has_account' => $hasAccount, + 'memo' => $company['memo'] ?? '' + ]; + } + + // 현재 세션의 회사 정보 + $currentCompany = $mycompany ?? ''; + + // 현재 선택된 테넌트 (세션에서 가져오거나 기본값) + $selectedTenantId = $_SESSION['eaccount_tenant_id'] ?? null; + + // 세션에 저장된 tenant_id가 없으면 '(주)주일기업'을 기본값으로 설정 + if ($selectedTenantId === null) { + // '(주)주일기업' 찾기 + $defaultTenant = null; + foreach ($tenants as $tenant) { + if (strpos($tenant['name'], '주일기업') !== false || + strpos($tenant['name'], '주일') !== false || + $tenant['user_id'] === 'juil5130') { + $defaultTenant = $tenant; + break; + } + } + + // '(주)주일기업'을 찾지 못하면 첫 번째 테넌트 사용 + if ($defaultTenant) { + $selectedTenantId = $defaultTenant['id']; + } elseif (count($tenants) > 0) { + $selectedTenantId = $tenants[0]['id']; + } + + if ($selectedTenantId) { + $_SESSION['eaccount_tenant_id'] = $selectedTenantId; + } + } + + echo json_encode([ + 'success' => true, + 'tenants' => $tenants, + 'current_tenant_id' => $selectedTenantId, + 'current_company' => $currentCompany + ], JSON_UNESCAPED_UNICODE); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '테넌트 목록 조회 실패: ' . $e->getMessage(), + 'tenants' => [], + 'current_tenant_id' => null + ], JSON_UNESCAPED_UNICODE); +} +?> \ No newline at end of file diff --git a/eaccount/api/set_tenant.php b/eaccount/api/set_tenant.php new file mode 100644 index 0000000..2fb49e5 --- /dev/null +++ b/eaccount/api/set_tenant.php @@ -0,0 +1,61 @@ +load(); + +require_once(getenv('DOCUMENT_ROOT') . '/session.php'); +require_once(getenv('DOCUMENT_ROOT') . '/lib/mydb.php'); + +$tenantId = $_POST['tenant_id'] ?? $_GET['tenant_id'] ?? ''; + +if (empty($tenantId)) { + echo json_encode([ + 'success' => false, + 'error' => '테넌트 ID가 필요합니다.' + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// DB에서 테넌트 존재 여부 확인 +try { + $pdo = db_connect(); + if (!$pdo) { + throw new Exception("Database connection failed."); + } + + $sql = "SELECT id, company_name FROM {$DB}.barobill_companies WHERE id = ?"; + $stmt = $pdo->prepare($sql); + $stmt->execute([$tenantId]); + $tenant = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$tenant) { + echo json_encode([ + 'success' => false, + 'error' => '유효하지 않은 테넌트 ID입니다.' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // 세션에 저장 + $_SESSION['eaccount_tenant_id'] = $tenantId; + + echo json_encode([ + 'success' => true, + 'tenant_id' => $tenantId, + 'tenant_name' => $tenant['company_name'], + 'message' => '테넌트가 변경되었습니다.' + ], JSON_UNESCAPED_UNICODE); + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '테넌트 변경 실패: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} +?> + diff --git a/eaccount/api/transactions.php b/eaccount/api/transactions.php new file mode 100644 index 0000000..2237409 --- /dev/null +++ b/eaccount/api/transactions.php @@ -0,0 +1,515 @@ + 0 // 전체 계좌 + ]); + + if ($accountResult['success']) { + $accountData = $accountResult['data']; + $accountList = []; + + // BankAccount 또는 BankAccountEx에서 계좌 목록 추출 + if (isset($accountData->BankAccount)) { + if (is_array($accountData->BankAccount)) { + $accountList = $accountData->BankAccount; + } else if (is_object($accountData->BankAccount)) { + $accountList = [$accountData->BankAccount]; + } + } else if (isset($accountData->BankAccountEx)) { + if (is_array($accountData->BankAccountEx)) { + $accountList = $accountData->BankAccountEx; + } else if (is_object($accountData->BankAccountEx)) { + $accountList = [$accountData->BankAccountEx]; + } + } + + // 각 계좌별로 거래 내역 조회 + $allLogs = []; + $allSummary = ['totalDeposit' => 0, 'totalWithdraw' => 0, 'count' => 0]; + $debugAllAccounts = []; + + foreach ($accountList as $accIndex => $acc) { + if (!is_object($acc)) continue; + + $accNum = $acc->BankAccountNum ?? ''; + if (empty($accNum) || (is_numeric($accNum) && $accNum < 0)) { + continue; // 에러 코드 스킵 + } + + // 각 계좌의 거래 내역 조회 + $accResult = callBarobillAccountSOAP('GetPeriodBankAccountTransLog', [ + 'ID' => $userId, + 'BankAccountNum' => $accNum, + 'StartDate' => $startDate, + 'EndDate' => $endDate, + 'TransDirection' => 1, + 'CountPerPage' => 1000, // 전체 조회를 위해 큰 값 사용 + 'CurrentPage' => 1, + 'OrderDirection' => 2 + ]); + + if ($accResult['success']) { + $accData = $accResult['data']; + + // 에러 코드 체크 + $errorCode = null; + if (isset($accData->CurrentPage) && is_numeric($accData->CurrentPage) && $accData->CurrentPage < 0) { + $errorCode = $accData->CurrentPage; + } elseif (isset($accData->BankAccountNum) && is_numeric($accData->BankAccountNum) && $accData->BankAccountNum < 0) { + $errorCode = $accData->BankAccountNum; + } + + // -25005, -25001은 데이터 없음 (정상) + if (!$errorCode || ($errorCode == -25005 || $errorCode == -25001)) { + // 거래 내역 파싱 + if (isset($accData->BankAccountLogList) && isset($accData->BankAccountLogList->BankAccountTransLog)) { + $rawLogs = is_array($accData->BankAccountLogList->BankAccountTransLog) + ? $accData->BankAccountLogList->BankAccountTransLog + : [$accData->BankAccountLogList->BankAccountTransLog]; + + foreach ($rawLogs as $log) { + $deposit = floatval($log->Deposit ?? 0); + $withdraw = floatval($log->Withdraw ?? 0); + + // 거래일시 파싱: TransDT 필드 사용 (YYYYMMDDHHmmss 형식) + $transDT = $log->TransDT ?? ''; + $transDate = ''; + $transTime = ''; + $dateTime = ''; + + if (!empty($transDT) && strlen($transDT) >= 14) { + // TransDT: "20251203100719" -> "2025-12-03 10:07:19" + $transDate = substr($transDT, 0, 8); // YYYYMMDD + $transTime = substr($transDT, 8, 6); // HHmmss + $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); + } else { + // 기존 방식도 지원 (다양한 필드명 확인) + $transDate = $log->TransDate ?? $log->TradeDate ?? $log->Date ?? ''; + $transTime = $log->TransTime ?? $log->TradeTime ?? $log->Time ?? ''; + + if (!empty($transDate) && !empty($transTime)) { + $dateStr = (string)$transDate; + $timeStr = (string)$transTime; + + if (strlen($dateStr) == 8 && strlen($timeStr) >= 4) { + $dateTime = substr($dateStr, 0, 4) . '-' . substr($dateStr, 4, 2) . '-' . substr($dateStr, 6, 2) . ' ' . + substr($timeStr, 0, 2) . ':' . substr($timeStr, 2, 2); + if (strlen($timeStr) >= 6) { + $dateTime .= ':' . substr($timeStr, 4, 2); + } + } elseif (strlen($dateStr) == 10 && strpos($dateStr, '-') !== false) { + $dateTime = $dateStr . ' ' . substr($timeStr, 0, 2) . ':' . substr($timeStr, 2, 2); + if (strlen($timeStr) >= 6) { + $dateTime .= ':' . substr($timeStr, 4, 2); + } + } + } + } + + // 적요 파싱: TransRemark1 필드 사용 + $summary = $log->TransRemark1 ?? $log->Summary ?? $log->Content ?? $log->Description ?? $log->Remark ?? $log->Note ?? ''; + + // 추가 적요 정보: TransRemark2 + $remark2 = $log->TransRemark2 ?? ''; + + // 거래 유형: TransType (예: CC) + $transType = $log->TransType ?? ''; + + // 거래 방향: TransDirection (예: 입금, 출금) + $transDirection = $log->TransDirection ?? ''; + + // 취급점: TransOffice + $transOffice = $log->TransOffice ?? ''; + + // 적요 정보 결합 + $fullSummary = $summary; + if (!empty($remark2)) { + $fullSummary = (!empty($fullSummary) ? $fullSummary . ' ' . $remark2 : $remark2); + } + if (!empty($transType)) { + $fullSummary = (!empty($fullSummary) ? $fullSummary . ' (' . $transType . ')' : '(' . $transType . ')'); + } + + // 보낸분/받는분 정보 (기존 필드명도 지원) + $cast = $log->Cast ?? $log->Counterpart ?? $log->Opponent ?? $log->Name ?? ''; + + // 취급점/수단 정보 + $branch = $transOffice ?: ($log->Branch ?? $log->HandlingBranch ?? $log->Method ?? ''); + + // 디버그: 첫 번째 계좌의 첫 번째 로그 정보 저장 + if ($accIndex === 0 && empty($debugAllAccounts)) { + $debugAllAccounts['first_account_first_log'] = [ + 'account_num' => $accNum, + 'log_raw_fields' => [], + 'parsed' => [ + 'transDate' => $transDate, + 'transTime' => $transTime, + 'transDateTime' => $dateTime, + 'summary' => $summary, + 'fullSummary' => $fullSummary, + 'cast' => $cast, + 'branch' => $branch + ] + ]; + // 원본 로그의 모든 필드 저장 + foreach ($log as $key => $value) { + $debugAllAccounts['first_account_first_log']['log_raw_fields'][$key] = + is_string($value) ? substr($value, 0, 100) : (is_numeric($value) ? $value : gettype($value)); + } + } + + $allLogs[] = [ + 'transDate' => $transDate, + 'transTime' => $transTime, + 'transDateTime' => $dateTime, + 'bankAccountNum' => $log->BankAccountNum ?? $accNum, + 'bankName' => $log->BankName ?? ($acc->BankName ?? ''), + 'deposit' => $deposit, + 'withdraw' => $withdraw, + 'depositFormatted' => number_format($deposit), + 'withdrawFormatted' => number_format($withdraw), + 'balance' => floatval($log->Balance ?? 0), + 'balanceFormatted' => number_format(floatval($log->Balance ?? 0)), + 'summary' => $fullSummary ?: $summary, + 'cast' => $cast, + 'memo' => $log->Memo ?? '', + 'identity' => $log->Identity ?? '', + 'branch' => $branch + ]; + + $allSummary['totalDeposit'] += $deposit; + $allSummary['totalWithdraw'] += $withdraw; + $allSummary['count']++; + } + } + } + } + } + + // 날짜/시간 기준으로 정렬 (최신순) + usort($allLogs, function($a, $b) { + $dateA = $a['transDate'] . $a['transTime']; + $dateB = $b['transDate'] . $b['transTime']; + return strcmp($dateB, $dateA); // 내림차순 + }); + + // 페이지네이션 계산 + $maxPageNum = ceil($allSummary['count'] / $limit); + $startIndex = ($page - 1) * $limit; + $paginatedLogs = array_slice($allLogs, $startIndex, $limit); + + $response = [ + 'success' => true, + 'data' => [ + 'logs' => $paginatedLogs, + 'pagination' => [ + 'currentPage' => $page, + 'countPerPage' => $limit, + 'maxPageNum' => $maxPageNum, + 'maxIndex' => $allSummary['count'] + ], + 'summary' => $allSummary + ] + ]; + + // 디버그 정보 추가 + if (!empty($debugAllAccounts)) { + $response['debug_all_accounts'] = $debugAllAccounts; + } + + // 디버그: 전체 계좌 조회 시 첫 번째 로그 확인 + if (!empty($paginatedLogs) && isset($paginatedLogs[0])) { + $response['debug_first_log_parsed'] = $paginatedLogs[0]; + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + return; + } + } + + // 단일 계좌 조회 (기존 로직) + $result = callBarobillAccountSOAP('GetPeriodBankAccountTransLog', [ + 'ID' => $userId, + 'BankAccountNum' => $bankAccountNum, + 'StartDate' => $startDate, + 'EndDate' => $endDate, + 'TransDirection' => 1, // 1:전체? (API 예제 값), 2:입금, 3:출금 추정 (확인 필요) - 우선 예제값 1 사용 + 'CountPerPage' => $limit, + 'CurrentPage' => $page, + 'OrderDirection' => 2 // 1:오름차순, 2:내림차순 + ]); + + if ($result['success']) { + $resultData = $result['data']; + + // 에러 코드 체크 (다양한 필드에서 확인) + $errorCode = null; + + // CurrentPage가 음수인 경우 + if (isset($resultData->CurrentPage) && is_numeric($resultData->CurrentPage) && $resultData->CurrentPage < 0) { + $errorCode = $resultData->CurrentPage; + } + // BankAccountNum이 음수인 경우 (예: -10002) + elseif (isset($resultData->BankAccountNum) && is_numeric($resultData->BankAccountNum) && $resultData->BankAccountNum < 0) { + $errorCode = $resultData->BankAccountNum; + } + + // -25005: 데이터 없음 (정상), -25001: 계좌 없음 (정상) + if ($errorCode && ($errorCode == -25005 || $errorCode == -25001)) { + echo json_encode([ + 'success' => true, + 'data' => [ + 'logs' => [], + 'summary' => ['totalDeposit' => 0, 'totalWithdraw' => 0, 'count' => 0], + 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1] + ] + ], JSON_UNESCAPED_UNICODE); + return; + } + + // 다른 오류 코드인 경우 + if ($errorCode) { + // 상세 에러 메시지 매핑 + $errorMsg = '계좌 내역 조회 실패: ' . $errorCode; + $errorMessages = [ + -10002 => '인증 실패 (-10002). CERTKEY가 올바르지 않거나 만료되었습니다. 바로빌 개발자센터에서 CERTKEY를 확인하세요.', + -50214 => '은행 로그인 실패 (-50214). 바로빌 사이트에서 계좌 비밀번호/인증서를 점검해주세요.', + -24005 => '사용자 정보 불일치 (-24005). 사업자번호를 확인해주세요.', + ]; + + if (isset($errorMessages[$errorCode])) { + $errorMsg = $errorMessages[$errorCode]; + } + + throw new Exception($errorMsg); + } + + // 데이터 파싱 (BankAccountTransLog) + $logs = []; + $rawLogs = []; + if (isset($resultData->BankAccountLogList) && isset($resultData->BankAccountLogList->BankAccountTransLog)) { + if (is_array($resultData->BankAccountLogList->BankAccountTransLog)) { + $rawLogs = $resultData->BankAccountLogList->BankAccountTransLog; + } else { + $rawLogs = [$resultData->BankAccountLogList->BankAccountTransLog]; + } + } + + // 디버그: 첫 번째 로그의 전체 구조 확인 + $debugInfo = []; + if (!empty($rawLogs) && is_object($rawLogs[0])) { + $firstLog = $rawLogs[0]; + $debugInfo['first_log_structure'] = []; + $debugInfo['first_log_all_keys'] = []; + foreach ($firstLog as $key => $value) { + $debugInfo['first_log_all_keys'][] = $key; + $debugInfo['first_log_structure'][$key] = [ + 'type' => gettype($value), + 'value' => is_string($value) ? $value : (is_numeric($value) ? $value : gettype($value)), + 'length' => is_string($value) ? strlen($value) : null + ]; + } + } + + $totalDeposit = 0; + $totalWithdraw = 0; + + foreach ($rawLogs as $logIndex => $log) { + $deposit = floatval($log->Deposit ?? 0); + $withdraw = floatval($log->Withdraw ?? 0); + + $totalDeposit += $deposit; + $totalWithdraw += $withdraw; + + // 거래일시 파싱: TransDT 필드 사용 (YYYYMMDDHHmmss 형식) + $transDT = $log->TransDT ?? ''; + $transDate = ''; + $transTime = ''; + $dateTime = ''; + + if (!empty($transDT) && strlen($transDT) >= 14) { + // TransDT: "20251203100719" -> "2025-12-03 10:07:19" + $transDate = substr($transDT, 0, 8); // YYYYMMDD + $transTime = substr($transDT, 8, 6); // HHmmss + $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); + } else { + // 기존 방식도 지원 (다양한 필드명 확인) + $transDate = $log->TransDate ?? $log->TradeDate ?? $log->Date ?? ''; + $transTime = $log->TransTime ?? $log->TradeTime ?? $log->Time ?? ''; + + if (!empty($transDate) && !empty($transTime)) { + $dateStr = (string)$transDate; + $timeStr = (string)$transTime; + + if (strlen($dateStr) == 8 && strlen($timeStr) >= 4) { + $dateTime = substr($dateStr, 0, 4) . '-' . substr($dateStr, 4, 2) . '-' . substr($dateStr, 6, 2) . ' ' . + substr($timeStr, 0, 2) . ':' . substr($timeStr, 2, 2); + if (strlen($timeStr) >= 6) { + $dateTime .= ':' . substr($timeStr, 4, 2); + } + } elseif (strlen($dateStr) == 10 && strpos($dateStr, '-') !== false) { + $dateTime = $dateStr . ' ' . substr($timeStr, 0, 2) . ':' . substr($timeStr, 2, 2); + if (strlen($timeStr) >= 6) { + $dateTime .= ':' . substr($timeStr, 4, 2); + } + } + } + } + + // 적요 파싱: TransRemark1 필드 사용 + $summary = $log->TransRemark1 ?? $log->Summary ?? $log->Content ?? $log->Description ?? $log->Remark ?? $log->Note ?? ''; + + // 추가 적요 정보: TransRemark2 + $remark2 = $log->TransRemark2 ?? ''; + + // 거래 유형: TransType (예: CC) + $transType = $log->TransType ?? ''; + + // 거래 방향: TransDirection (예: 입금, 출금) + $transDirection = $log->TransDirection ?? ''; + + // 취급점: TransOffice + $transOffice = $log->TransOffice ?? ''; + + // 적요 정보 결합 + $fullSummary = $summary; + if (!empty($remark2)) { + $fullSummary = (!empty($fullSummary) ? $fullSummary . ' ' . $remark2 : $remark2); + } + if (!empty($transType)) { + $fullSummary = (!empty($fullSummary) ? $fullSummary . ' (' . $transType . ')' : '(' . $transType . ')'); + } + + // 보낸분/받는분 정보 (기존 필드명도 지원) + $cast = $log->Cast ?? $log->Counterpart ?? $log->Opponent ?? $log->Name ?? ''; + + // 취급점/수단 정보 + $branch = $transOffice ?: ($log->Branch ?? $log->HandlingBranch ?? $log->Method ?? ''); + + $logs[] = [ + 'transDate' => $transDate, + 'transTime' => $transTime, + 'transDateTime' => $dateTime, + 'bankAccountNum' => $log->BankAccountNum ?? '', + 'bankName' => $log->BankName ?? '', + 'deposit' => $deposit, + 'withdraw' => $withdraw, + 'depositFormatted' => number_format($deposit), + 'withdrawFormatted' => number_format($withdraw), + 'balance' => floatval($log->Balance ?? 0), + 'balanceFormatted' => number_format(floatval($log->Balance ?? 0)), + 'summary' => $fullSummary ?: $summary, // 적요 (통합 정보) + 'cast' => $cast, // 보낸분/받는분 + 'memo' => $log->Memo ?? '', + 'identity' => $log->Identity ?? '', // 고유번호 + 'branch' => $branch, // 취급점/수단 + 'rawData' => json_encode($log, JSON_UNESCAPED_UNICODE) // 디버깅용 원본 데이터 + ]; + } + + $response = [ + 'success' => true, + 'data' => [ + 'logs' => $logs, + 'pagination' => [ + 'currentPage' => $resultData->CurrentPage ?? 1, + 'countPerPage' => $resultData->CountPerPage ?? 50, + 'maxPageNum' => $resultData->MaxPageNum ?? 1, + 'maxIndex' => $resultData->MaxIndex ?? 0 + ], + 'summary' => [ + 'totalDeposit' => $totalDeposit, + 'totalWithdraw' => $totalWithdraw, + 'count' => count($logs) + ] + ] + ]; + + // 디버그 정보 추가 + if (isset($result['debug'])) { + $response['debug'] = $result['debug']; + } + + // API 응답 구조 디버그 정보 추가 + if (!empty($debugInfo)) { + $response['debug_api_structure'] = $debugInfo; + } + + // 첫 번째 로그의 원본 데이터도 포함 (필드명 확인용) + if (!empty($rawLogs) && is_object($rawLogs[0])) { + $response['debug_first_log_raw'] = []; + foreach ($rawLogs[0] as $key => $value) { + $response['debug_first_log_raw'][$key] = is_string($value) ? $value : (is_numeric($value) ? $value : gettype($value)); + } + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + + } else { + // API Error Handling (Graceful Fallback) + // If API fails (e.g., SoapClient missing), return empty list with warning + // so the UI doesn't break. + $response = [ + 'success' => true, // Masquerade as success to render empty table + 'data' => [ + 'logs' => [], + 'pagination' => [ + 'currentPage' => 1, + 'countPerPage' => $limit, + 'maxPageNum' => 1, + 'maxIndex' => 0 + ], + 'summary' => [ + 'totalDeposit' => 0, + 'totalWithdraw' => 0, + 'count' => 0 + ] + ], + 'warning' => 'API 연동 실패: ' . $result['error'], // Custom warning field + 'api_error_code' => $result['error_code'] ?? null + ]; + + // Add debug info if available + if (isset($result['debug'])) { + $response['debug'] = $result['debug']; + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + } +} catch (Throwable $e) { + // Global Exception/Error Handling + echo json_encode([ + 'success' => true, // Return true to avoid UI breakage + 'data' => [ + 'logs' => [], + 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1], + 'summary' => ['totalDeposit' => 0, 'totalWithdraw' => 0, 'count' => 0] + ], + 'warning' => '시스템 오류: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} +?> diff --git a/eaccount/api/usage.php b/eaccount/api/usage.php new file mode 100644 index 0000000..17648bc --- /dev/null +++ b/eaccount/api/usage.php @@ -0,0 +1,397 @@ +load(); + +require_once('barobill_card_config.php'); + +// 디버그 모드 +$debugMode = isset($_GET['debug']) && $_GET['debug'] == '1'; + +try { + $type = $_GET['type'] ?? 'period'; + $cardNum = $_GET['cardNum'] ?? ''; + $page = max(1, intval($_GET['page'] ?? 1)); + $limit = min(100, max(10, intval($_GET['limit'] ?? 50))); + $orderDirection = intval($_GET['order'] ?? 2); // 2: 내림차순 (최신순) + + $result = null; + + // cardNum이 빈 값이면 전체 카드 조회 (각 카드별로 조회 후 병합) + if (empty($cardNum)) { + // 등록된 카드 목록 조회 + $cardsResult = getCardList(1); // 사용 가능한 카드만 + if (!$cardsResult['success'] || empty($cardsResult['data'])) { + $result = [ + 'success' => true, + 'data' => [ + 'currentPage' => 1, + 'countPerPage' => $limit, + 'maxPageNum' => 1, + 'maxIndex' => 0, + 'logs' => [] + ] + ]; + } else { + // 각 카드별로 조회 후 병합 + $allLogs = []; + foreach ($cardsResult['data'] as $card) { + $cardNumToQuery = $card->CardNum ?? ''; + if (empty($cardNumToQuery)) continue; + + switch ($type) { + case 'daily': + $baseDate = $_GET['baseDate'] ?? date('Ymd'); + $tempResult = getDailyCardUsage($cardNumToQuery, $baseDate, 100, 1, $orderDirection); + break; + case 'monthly': + $baseMonth = $_GET['baseMonth'] ?? date('Ym'); + $tempResult = getMonthlyCardUsage($cardNumToQuery, $baseMonth, 100, 1, $orderDirection); + break; + case 'period': + default: + $startDate = $_GET['startDate'] ?? date('Ymd', strtotime('-30 days')); + $endDate = $_GET['endDate'] ?? date('Ymd'); + $tempResult = getPeriodCardUsage($cardNumToQuery, $startDate, $endDate, 100, 1, $orderDirection); + break; + } + + if ($tempResult['success'] && !empty($tempResult['data']['logs'])) { + $allLogs = array_merge($allLogs, $tempResult['data']['logs']); + } + } + + // UseDT 기준으로 정렬 + usort($allLogs, function($a, $b) use ($orderDirection) { + $aTime = $a->UseDT ?? ''; + $bTime = $b->UseDT ?? ''; + return $orderDirection == 1 ? strcmp($aTime, $bTime) : strcmp($bTime, $aTime); + }); + + // 페이징 처리 + $totalCount = count($allLogs); + $maxPageNum = ceil($totalCount / $limit); + $offset = ($page - 1) * $limit; + $pagedLogs = array_slice($allLogs, $offset, $limit); + + $result = [ + 'success' => true, + 'data' => [ + 'currentPage' => $page, + 'countPerPage' => $limit, + 'maxPageNum' => $maxPageNum, + 'maxIndex' => $totalCount, + 'logs' => $pagedLogs + ] + ]; + } + } else { + // 특정 카드 조회 + switch ($type) { + case 'daily': + $baseDate = $_GET['baseDate'] ?? date('Ymd'); + $result = getDailyCardUsage($cardNum, $baseDate, $limit, $page, $orderDirection); + break; + + case 'monthly': + $baseMonth = $_GET['baseMonth'] ?? date('Ym'); + $result = getMonthlyCardUsage($cardNum, $baseMonth, $limit, $page, $orderDirection); + break; + + case 'period': + default: + $startDate = $_GET['startDate'] ?? date('Ymd', strtotime('-30 days')); + $endDate = $_GET['endDate'] ?? date('Ymd'); + $result = getPeriodCardUsage($cardNum, $startDate, $endDate, $limit, $page, $orderDirection); + break; + } + } + + if ($result['success']) { + $logs = []; + + // 디버그: raw 로그 데이터 출력 + if ($debugMode && !empty($result['data']['logs'])) { + $firstLog = $result['data']['logs'][0]; + error_log('CardApprovalLog raw data: ' . print_r($firstLog, true)); + // 디버그: 모든 필드명 확인 + if (is_object($firstLog)) { + $fields = get_object_vars($firstLog); + error_log('CardApprovalLog fields: ' . implode(', ', array_keys($fields))); + error_log('ApprovalAmount value: ' . ($firstLog->ApprovalAmount ?? 'NOT SET')); + error_log('Amount value: ' . ($firstLog->Amount ?? 'NOT SET')); + error_log('TotalAmount value: ' . ($firstLog->TotalAmount ?? 'NOT SET')); + } + } + + foreach ($result['data']['logs'] as $log) { + // UseDT 형식: YYYYMMDDHHMMSS + $useDT = $log->UseDT ?? ''; + $approvalDate = ''; + $approvalTime = ''; + if (strlen($useDT) >= 8) { + $approvalDate = substr($useDT, 0, 4) . '-' . substr($useDT, 4, 2) . '-' . substr($useDT, 6, 2); + } + if (strlen($useDT) >= 14) { + $approvalTime = substr($useDT, 8, 2) . ':' . substr($useDT, 10, 2) . ':' . substr($useDT, 12, 2); + } elseif (strlen($useDT) >= 12) { + $approvalTime = substr($useDT, 8, 2) . ':' . substr($useDT, 10, 2); + } + + $logs[] = [ + 'cardNum' => maskCardNumber($log->CardNum ?? ''), + 'cardNumFull' => $log->CardNum ?? '', + 'approvalNum' => $log->ApprovalNum ?? '', + 'approvalDate' => $approvalDate, + 'approvalTime' => $approvalTime, + 'approvalDateTime' => $approvalDate . ' ' . $approvalTime, + 'merchantName' => $log->UseStoreName ?? '', + 'merchantBizNum' => $log->UseStoreCorpNum ?? '', + // 금액 필드: 여러 가능한 필드명 시도 + // ApprovalAmount가 실제 승인금액 (화면에 표시할 금액) + 'amount' => intval($log->ApprovalAmount ?? 0), + 'amountFormatted' => number_format(intval($log->ApprovalAmount ?? 0)), + 'vat' => intval($log->Tax ?? 0), + 'vatFormatted' => number_format(intval($log->Tax ?? 0)), + 'serviceCharge' => intval($log->ServiceCharge ?? 0), + // totalAmount는 화면에서 사용하므로 ApprovalAmount를 사용 + 'totalAmount' => intval($log->ApprovalAmount ?? 0), + 'totalAmountFormatted' => number_format(intval($log->ApprovalAmount ?? 0)), + 'approvalType' => $log->ApprovalType ?? '', + 'approvalTypeName' => getApprovalTypeName($log->ApprovalType ?? ''), + 'installment' => $log->PaymentPlan ?? '', + 'installmentName' => getInstallmentName($log->PaymentPlan ?? ''), + 'currencyCode' => $log->CurrencyCode ?? 'KRW', + 'memo' => $log->Memo ?? '', + 'cardCompany' => $log->CardCompany ?? '', + 'cardCompanyName' => getCardCompanyNameFromLog($log->CardCompany ?? ''), + // 추가 필드 + 'useKey' => $log->UseKey ?? '', + 'storeAddress' => $log->UseStoreAddr ?? '', + 'storeCeo' => $log->UseStoreCeo ?? '', + 'storeBizType' => $log->UseStoreBizType ?? '', + 'storeTel' => $log->UseStoreTel ?? '' + ]; + } + + // 통계 계산 + $totalAmount = array_sum(array_column($logs, 'totalAmount')); + $approvalCount = count(array_filter($logs, function($l) { return $l['approvalType'] == '1'; })); + $cancelCount = count(array_filter($logs, function($l) { return $l['approvalType'] == '2'; })); + + $response = [ + 'success' => true, + 'data' => [ + 'logs' => $logs, + 'pagination' => [ + 'currentPage' => $result['data']['currentPage'], + 'countPerPage' => $result['data']['countPerPage'], + 'maxPageNum' => $result['data']['maxPageNum'], + 'totalCount' => $result['data']['maxIndex'] + ], + 'summary' => [ + 'totalAmount' => $totalAmount, + 'totalAmountFormatted' => number_format($totalAmount), + 'count' => count($logs), + 'approvalCount' => $approvalCount, + 'cancelCount' => $cancelCount + ] + ] + ]; + + // 디버그 정보 추가 (성공한 경우에도 항상 첫 번째 로그의 필드 정보 출력) + if (!empty($result['data']['logs'])) { + $firstLog = $result['data']['logs'][0]; + if (is_object($firstLog)) { + // 모든 필드를 배열로 변환 + $allFields = get_object_vars($firstLog); + $fieldNames = array_keys($allFields); + + // 금액 관련 필드 찾기 (대소문자 구분 없이) + $amountFields = []; + foreach ($fieldNames as $fieldName) { + if (stripos($fieldName, 'amount') !== false || + stripos($fieldName, 'cost') !== false || + stripos($fieldName, 'price') !== false || + stripos($fieldName, '금액') !== false) { + $amountFields[$fieldName] = (string)($firstLog->$fieldName ?? 'NULL'); + } + } + + // 디버그 모드일 때만 상세 정보 출력 + if ($debugMode) { + $response['debug'] = [ + 'userId' => getBarobillUserId(), + 'params' => $_GET, + 'firstLogFields' => $fieldNames, + 'firstLogAllValues' => array_map(function($v) { + return is_string($v) ? $v : (is_numeric($v) ? (string)$v : gettype($v)); + }, $allFields), + 'amountFields' => $amountFields + ]; + } else { + // 디버그 모드가 아니어도 금액 필드 정보는 항상 포함 (문제 해결용) + $response['debug'] = [ + 'amountFields' => $amountFields, + 'allFields' => $fieldNames + ]; + } + } + } elseif ($debugMode) { + $response['debug'] = [ + 'userId' => getBarobillUserId(), + 'params' => $_GET, + 'message' => 'No logs found' + ]; + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + } else { + // API Error Handling (Graceful Fallback) + // If API fails (e.g., SoapClient missing), return empty list with warning + $response = [ + 'success' => true, // Masquerade as success to render empty table + 'data' => [ + 'logs' => [], + 'pagination' => [ + 'currentPage' => 1, + 'countPerPage' => $limit, + 'maxPageNum' => 1, + 'totalCount' => 0 + ], + 'summary' => [ + 'totalAmount' => 0, + 'totalAmountFormatted' => '0', + 'count' => 0, + 'approvalCount' => 0, + 'cancelCount' => 0 + ] + ], + 'warning' => 'API 연동 실패: ' . $result['error'], + 'api_error_code' => $result['error_code'] ?? null + ]; + + // 디버그 정보 추가 + if ($debugMode) { + $response['debug'] = [ + 'userId' => getBarobillUserId(), + 'params' => $_GET + ]; + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + } +} catch (Throwable $e) { + echo json_encode([ + 'success' => true, // Return true to avoid UI breakage + 'data' => [ + 'logs' => [], + 'pagination' => ['currentPage' => 1, 'maxPageNum' => 1, 'totalCount' => 0], + 'summary' => ['totalAmount' => 0, 'count' => 0] + ], + 'warning' => '시스템 오류: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} + +/** + * 카드번호 마스킹 + */ +function maskCardNumber($cardNum) { + if (strlen($cardNum) < 8) return $cardNum; + return substr($cardNum, 0, 4) . '-****-****-' . substr($cardNum, -4); +} + +/** + * 날짜 포맷팅 + */ +function formatDate($date) { + if (strlen($date) === 8) { + return substr($date, 0, 4) . '-' . substr($date, 4, 2) . '-' . substr($date, 6, 2); + } + return $date; +} + +/** + * 시간 포맷팅 + */ +function formatTime($time) { + if (strlen($time) === 6) { + return substr($time, 0, 2) . ':' . substr($time, 2, 2) . ':' . substr($time, 4, 2); + } elseif (strlen($time) === 4) { + return substr($time, 0, 2) . ':' . substr($time, 2, 2); + } + return $time; +} + +/** + * 승인 유형 이름 + */ +function getApprovalTypeName($type) { + $types = [ + '1' => '승인', + '2' => '취소' + ]; + return $types[$type] ?? $type; +} + +/** + * 할부 이름 + */ +function getInstallmentName($installment) { + if (empty($installment) || $installment == '0' || $installment == '00') { + return '일시불'; + } + return $installment . '개월'; +} + +/** + * 카드사 이름 (로그용) + */ +function getCardCompanyNameFromLog($code) { + $companies = [ + '01' => '비씨', + '02' => 'KB국민', + '03' => '하나(외환)', + '04' => '삼성', + '06' => '신한', + '07' => '현대', + '08' => '롯데', + '11' => 'NH농협', + '12' => '수협', + '13' => '씨티', + '14' => '우리', + '15' => '광주', + '16' => '전북', + '21' => '하나', + '22' => '제주', + '23' => 'SC제일', + '25' => 'KDB산업', + '26' => 'IBK기업', + '27' => '새마을금고', + '28' => '신협', + '29' => '저축은행', + '30' => '우체국', + '31' => '카카오뱅크', + '32' => 'K뱅크', + '33' => '토스뱅크' + ]; + return $companies[$code] ?? $code; +} +?> + diff --git a/eaccount/index.php b/eaccount/index.php new file mode 100644 index 0000000..5ada23d --- /dev/null +++ b/eaccount/index.php @@ -0,0 +1,1421 @@ + + + + + + 계좌 입출금내역 조회 - 바로빌 연동 + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/ecard/README.md b/ecard/README.md new file mode 100644 index 0000000..946f3b9 --- /dev/null +++ b/ecard/README.md @@ -0,0 +1,138 @@ +# 법인카드 사용내역 조회 모듈 + +바로빌 API를 이용한 법인카드 사용내역 조회 모듈입니다. + +## 📋 기능 + +- 등록된 카드 목록 조회 +- 기간별/일별/월별 카드 사용내역 조회 +- 사용금액 통계 (총 사용금액, 사용건수, 취소건수) +- 페이지네이션 지원 + +## 🔧 설정 + +### 1. API 키 설정 (기존 etax 모듈과 공유) + +다음 파일들이 필요합니다 (`/apikey/` 폴더): + +| 파일명 | 설명 | 예시 | +|--------|------|------| +| `barobill_cert_key.txt` | 바로빌 CERTKEY (인증서 키) | `ABC123...` | +| `barobill_corp_num.txt` | 사업자번호 (하이픈 제외) | `6648603713` | +| `barobill_test_mode.txt` | 테스트 모드 (선택) | `test` 또는 `true` | + +### 2. 바로빌 카드 등록 + +카드 사용내역을 조회하려면 **바로빌 웹사이트**에서 카드를 먼저 등록해야 합니다. + +1. [바로빌](https://www.barobill.co.kr) 로그인 +2. 카드조회 서비스 신청 +3. 카드 등록 (카드사 웹 ID/비밀번호 필요) + +## 📁 파일 구조 + +``` +ecard/ +├── index.php # 메인 UI (React 기반) +├── api/ +│ ├── barobill_card_config.php # 바로빌 카드 API 설정 +│ ├── cards.php # 등록된 카드 목록 API +│ └── usage.php # 카드 사용내역 조회 API +└── README.md # 이 문서 +``` + +## 🔌 API 엔드포인트 + +### 카드 목록 조회 +``` +GET /ecard/api/cards.php +``` + +**응답 예시:** +```json +{ + "success": true, + "cards": [ + { + "cardNum": "1234-****-****-5678", + "cardCompany": "02", + "cardCompanyName": "KB국민", + "alias": "법인카드1", + "status": "1", + "statusName": "정상" + } + ], + "count": 1 +} +``` + +### 사용내역 조회 +``` +GET /ecard/api/usage.php?type=period&startDate=20241101&endDate=20241130 +``` + +**파라미터:** +| 파라미터 | 설명 | 기본값 | +|---------|------|--------| +| `type` | 조회 타입 (period/daily/monthly) | `period` | +| `cardNum` | 카드번호 (빈값=전체) | - | +| `startDate` | 시작일 (YYYYMMDD) - period용 | 30일 전 | +| `endDate` | 종료일 (YYYYMMDD) - period용 | 오늘 | +| `baseDate` | 기준일 (YYYYMMDD) - daily용 | 오늘 | +| `baseMonth` | 기준월 (YYYYMM) - monthly용 | 이번달 | +| `page` | 페이지 번호 | `1` | +| `limit` | 페이지당 건수 | `50` | + +**응답 예시:** +```json +{ + "success": true, + "data": { + "logs": [ + { + "cardNum": "1234-****-****-5678", + "approvalNum": "12345678", + "approvalDate": "2024-11-15", + "approvalTime": "14:30:25", + "merchantName": "스타벅스 강남점", + "amount": 5000, + "totalAmountFormatted": "5,000", + "approvalTypeName": "승인", + "installmentName": "일시불" + } + ], + "pagination": { + "currentPage": 1, + "countPerPage": 50, + "maxPageNum": 1, + "totalCount": 15 + }, + "summary": { + "totalAmount": 150000, + "count": 15, + "approvalCount": 14, + "cancelCount": 1 + } + } +} +``` + +## 🎨 UI 기능 + +- **카드 선택**: 특정 카드 또는 전체 카드 조회 +- **기간 설정**: 날짜 범위 직접 선택 또는 빠른 선택 (오늘, 7일, 30일, 3개월, 6개월) +- **통계 대시보드**: 총 사용금액, 사용건수, 취소건수 표시 +- **사용내역 테이블**: 승인일시, 가맹점명, 금액, 할부, 승인/취소 구분 + +## ⚠️ 주의사항 + +1. 바로빌 카드조회 서비스는 **유료 서비스**입니다. +2. 카드 등록 시 **카드사 웹 ID/비밀번호**가 필요합니다. +3. 카드사에서 데이터를 수집하므로 **실시간 조회가 아닐 수 있습니다** (보통 1일 1회 수집). +4. 테스트 환경에서는 실제 데이터가 아닌 테스트 데이터가 조회됩니다. + +## 🔗 참고 문서 + +- [바로빌 카드조회 API 레퍼런스](https://dev.barobill.co.kr/docs/references/카드조회-API) +- [바로빌 개발자센터](https://dev.barobill.co.kr) + diff --git a/ecard/api/barobill_card_config.php b/ecard/api/barobill_card_config.php new file mode 100644 index 0000000..cd95c78 --- /dev/null +++ b/ecard/api/barobill_card_config.php @@ -0,0 +1,510 @@ + true, + 'encoding' => 'UTF-8', + 'exceptions' => true, + 'connection_timeout' => 30 + ]); + } catch (Exception $e) { + error_log('바로빌 카드 SOAP 클라이언트 생성 실패: ' . $e->getMessage()); + } +} + +/** + * 바로빌 카드 SOAP 웹서비스 호출 함수 + * + * @param string $method SOAP 메서드명 + * @param array $params SOAP 메서드 파라미터 + * @return array 응답 데이터 + */ +function callBarobillCardSOAP($method, $params = []) { + global $barobillCardSoapClient, $barobillCertKey, $barobillCorpNum, $isTestMode; + + if (!$barobillCardSoapClient) { + return [ + 'success' => false, + 'error' => '바로빌 카드 SOAP 클라이언트가 초기화되지 않았습니다. CERTKEY를 확인하세요.', + 'error_detail' => [ + 'cert_key_file' => $_SERVER['DOCUMENT_ROOT'] . '/apikey/barobill_cert_key.txt', + 'soap_url' => $isTestMode ? 'https://testws.baroservice.com/CARD.asmx?WSDL' : 'https://ws.baroservice.com/CARD.asmx?WSDL' + ] + ]; + } + + if (empty($barobillCertKey) && !$isTestMode) { + return [ + 'success' => false, + 'error' => 'CERTKEY가 설정되지 않았습니다. apikey/barobill_cert_key.txt 파일을 확인하세요.' + ]; + } + + if (empty($barobillCorpNum)) { + return [ + 'success' => false, + 'error' => '사업자번호가 설정되지 않았습니다. apikey/barobill_corp_num.txt 파일을 확인하세요.' + ]; + } + + // CERTKEY와 CorpNum 자동 추가 + if (!isset($params['CERTKEY'])) { + $params['CERTKEY'] = $barobillCertKey; + } + if (!isset($params['CorpNum'])) { + $params['CorpNum'] = $barobillCorpNum; + } + + try { + error_log('바로빌 카드 API 호출 - Method: ' . $method . ', CorpNum: ' . $barobillCorpNum); + + $result = $barobillCardSoapClient->$method($params); + + $resultProperty = $method . 'Result'; + if (isset($result->$resultProperty)) { + $resultData = $result->$resultProperty; + + // 에러 코드 체크 (음수 값) + if (is_numeric($resultData) && $resultData < 0) { + return [ + 'success' => false, + 'error' => '바로빌 카드 API 오류 코드: ' . $resultData, + 'error_code' => $resultData + ]; + } + + return [ + 'success' => true, + 'data' => $resultData + ]; + } + + return [ + 'success' => true, + 'data' => $result + ]; + + } catch (SoapFault $e) { + return [ + 'success' => false, + 'error' => 'SOAP 오류: ' . $e->getMessage(), + 'error_code' => $e->getCode() + ]; + } catch (Exception $e) { + return [ + 'success' => false, + 'error' => 'API 호출 오류: ' . $e->getMessage() + ]; + } +} + +/** + * 등록된 카드 목록 조회 (GetCardEx2 API 사용) + * API 레퍼런스: https://dev.barobill.co.kr/docs/references/카드조회-API#GetCardEx2 + * + * 데이터 출처: 바로빌 서버에 등록된 카드 정보 + * - 카드 등록은 바로빌 웹사이트(https://www.barobill.co.kr)에서 직접 해야 함 + * - 이 함수는 등록된 카드 정보를 조회만 수행 + * + * 반환되는 카드 정보: + * - CardNum: 카드번호 (바로빌에 등록된 카드번호) + * - CardCompanyCode: 카드사 코드 (01=BC, 02=KB, 04=삼성, 06=신한 등) + * - CardCompanyName: 카드사 이름 + * - WebId: 카드사 웹 ID (카드사 홈페이지 로그인 ID) + * - Alias: 카드 별칭 + * - CardType: 카드 종류 (1=개인, 2=법인) + * - Status: 카드 상태 (0=대기중, 1=정상, 2=해지, 3=수집오류, 4=일시중지) + * - CollectCycle: 수집주기 (1=1일1회, 2=1일2회, 3=1일3회) + * + * @param int $availOnly 0: 전체, 1: 사용가능한 카드만 + * @return array 카드 목록 + */ +function getCardList($availOnly = 0) { + $result = callBarobillCardSOAP('GetCardEx2', [ + 'AvailOnly' => $availOnly + ]); + + if (!$result['success']) { + return $result; + } + + $cards = []; + $data = $result['data']; + + // GetCardEx2는 CardEx 배열을 반환 + if (!isset($data->CardEx)) { + return ['success' => true, 'data' => []]; + } + + if (!is_array($data->CardEx)) { + $cards = [$data->CardEx]; + } else { + $cards = $data->CardEx; + } + + // 에러 체크: CardNum이 음수면 에러 코드 + if (count($cards) == 1 && isset($cards[0]->CardNum) && $cards[0]->CardNum < 0) { + return [ + 'success' => false, + 'error' => '카드 목록 조회 실패', + 'error_code' => $cards[0]->CardNum + ]; + } + + return ['success' => true, 'data' => $cards]; +} + +/** + * 기간별 카드 사용내역 조회 + * + * 데이터 출처: 바로빌 서버에서 수집된 카드 사용내역 + * - 바로빌이 카드사에서 자동으로 수집한 사용내역을 조회 + * - 수집주기(CollectCycle)에 따라 1일 1회~3회 자동 수집 + * + * 반환되는 사용내역 정보: + * - CardNum: 카드번호 + * - UseDT: 사용일시 (YYYYMMDDHHMMSS) + * - UseStoreName: 가맹점명 + * - UseStoreCorpNum: 가맹점 사업자번호 + * - ApprovalAmount: 승인금액 + * - Tax: 부가세 + * - ServiceCharge: 봉사료 + * - ApprovalType: 승인유형 (1=승인, 2=취소) + * - PaymentPlan: 할부개월 (0=일시불) + * - ApprovalNum: 승인번호 + * + * @param string $cardNum 카드번호 (빈값이면 전체) + * @param string $startDate 시작일 (YYYYMMDD) + * @param string $endDate 종료일 (YYYYMMDD) + * @param int $countPerPage 페이지당 건수 + * @param int $currentPage 현재 페이지 + * @param int $orderDirection 정렬 (1: 오름차순, 2: 내림차순) + * @param string $userId 바로빌 사용자 ID (빈값이면 전체) + * @return array 사용내역 + */ +function getPeriodCardUsage($cardNum = '', $startDate = '', $endDate = '', $countPerPage = 50, $currentPage = 1, $orderDirection = 2, $userId = '') { + global $barobillCorpNum; + + // 바로빌 사용자 ID 파일에서 읽기 (없으면 빈값) + $barobillUserId = getBarobillUserId(); + if (!empty($userId)) { + $barobillUserId = $userId; + } + + $result = callBarobillCardSOAP('GetPeriodCardApprovalLog', [ + 'ID' => $barobillUserId, + 'CardNum' => $cardNum, + 'StartDate' => $startDate, + 'EndDate' => $endDate, + 'CountPerPage' => $countPerPage, + 'CurrentPage' => $currentPage, + 'OrderDirection' => $orderDirection + ]); + + if (!$result['success']) { + return $result; + } + + return parseCardUsageResult($result['data']); +} + +/** + * 일별 카드 사용내역 조회 + * + * @param string $cardNum 카드번호 (빈값이면 전체) + * @param string $baseDate 기준일 (YYYYMMDD) + * @param int $countPerPage 페이지당 건수 + * @param int $currentPage 현재 페이지 + * @param int $orderDirection 정렬 (1: 오름차순, 2: 내림차순) + * @param string $userId 바로빌 사용자 ID (빈값이면 전체) + * @return array 사용내역 + */ +function getDailyCardUsage($cardNum = '', $baseDate = '', $countPerPage = 50, $currentPage = 1, $orderDirection = 2, $userId = '') { + $barobillUserId = getBarobillUserId(); + if (!empty($userId)) { + $barobillUserId = $userId; + } + + $result = callBarobillCardSOAP('GetDailyCardApprovalLog', [ + 'ID' => $barobillUserId, + 'CardNum' => $cardNum, + 'BaseDate' => $baseDate, + 'CountPerPage' => $countPerPage, + 'CurrentPage' => $currentPage, + 'OrderDirection' => $orderDirection + ]); + + if (!$result['success']) { + return $result; + } + + return parseCardUsageResult($result['data']); +} + +/** + * 월별 카드 사용내역 조회 + * + * @param string $cardNum 카드번호 (빈값이면 전체) + * @param string $baseMonth 기준월 (YYYYMM) + * @param int $countPerPage 페이지당 건수 + * @param int $currentPage 현재 페이지 + * @param int $orderDirection 정렬 (1: 오름차순, 2: 내림차순) + * @param string $userId 바로빌 사용자 ID (빈값이면 전체) + * @return array 사용내역 + */ +function getMonthlyCardUsage($cardNum = '', $baseMonth = '', $countPerPage = 50, $currentPage = 1, $orderDirection = 2, $userId = '') { + $barobillUserId = getBarobillUserId(); + if (!empty($userId)) { + $barobillUserId = $userId; + } + + $result = callBarobillCardSOAP('GetMonthlyCardApprovalLog', [ + 'ID' => $barobillUserId, + 'CardNum' => $cardNum, + 'BaseMonth' => $baseMonth, + 'CountPerPage' => $countPerPage, + 'CurrentPage' => $currentPage, + 'OrderDirection' => $orderDirection + ]); + + if (!$result['success']) { + return $result; + } + + return parseCardUsageResult($result['data']); +} + +/** + * 카드 사용내역 결과 파싱 + * + * @param object $data SOAP 응답 데이터 + * @return array 파싱된 결과 + */ +function parseCardUsageResult($data) { + // 에러 체크 + if (isset($data->CurrentPage) && $data->CurrentPage < 0) { + $errorCode = $data->CurrentPage; + + // -24005: 조회 데이터 없음 (정상 케이스로 처리) + // -24001: 등록된 카드 없음 + // -24002: 조회 기간 오류 + if ($errorCode == -24005 || $errorCode == -24001) { + // 데이터 없음 - 빈 배열 반환 (에러가 아님) + return [ + 'success' => true, + 'data' => [ + 'currentPage' => 1, + 'countPerPage' => 50, + 'maxPageNum' => 1, + 'maxIndex' => 0, + 'logs' => [] + ] + ]; + } + + return [ + 'success' => false, + 'error' => '카드 사용내역 조회 실패', + 'error_code' => $errorCode + ]; + } + + $logs = []; + if (isset($data->CardLogList) && isset($data->CardLogList->CardApprovalLog)) { + if (!is_array($data->CardLogList->CardApprovalLog)) { + $logs = [$data->CardLogList->CardApprovalLog]; + } else { + $logs = $data->CardLogList->CardApprovalLog; + } + } + + return [ + 'success' => true, + 'data' => [ + 'currentPage' => $data->CurrentPage ?? 1, + 'countPerPage' => $data->CountPerPage ?? 50, + 'maxPageNum' => $data->MaxPageNum ?? 1, + 'maxIndex' => $data->MaxIndex ?? 0, + 'logs' => $logs + ] + ]; +} + +/** + * 카드 등록 + * + * ⚠️ 주의: 현재 이 함수는 사용되지 않습니다. + * 카드 등록은 바로빌 웹사이트(https://www.barobill.co.kr)에서 직접 해야 합니다. + * + * 카드 등록 시 필요한 정보: + * - CardNum: 카드번호 (예: "1234567890123456") + * - CardCompany: 카드사 코드 (예: "04"=삼성, "06"=신한, "02"=KB) + * - CardType: 카드 종류 ("1"=개인, "2"=법인) + * - WebId: 카드사 웹 ID (카드사 홈페이지 로그인 ID) + * - WebPwd: 카드사 웹 비밀번호 (카드사 홈페이지 로그인 비밀번호) + * - Alias: 카드 별칭 (선택사항, 예: "법인카드1") + * - CollectCycle: 수집주기 ("1"=1일1회, "2"=1일2회, "3"=1일3회) + * - Usage: 용도 ("1"=세금계산서, "2"=기타) + * + * 카드사 코드 참고: + * - 01: BC카드 + * - 02: KB국민카드 + * - 04: 삼성카드 + * - 06: 신한카드 + * - 07: 현대카드 + * - 08: 롯데카드 + * - 11: NH농협카드 + * - 21: 하나카드 + * + * @param array $cardData 카드 데이터 + * @return array 응답 데이터 + */ +function registerCard($cardData) { + return callBarobillCardSOAP('RegistCardEx', [ + 'CollectCycle' => $cardData['collectCycle'] ?? '1', // 수집주기 (1: 1일 1회) + 'CardCompany' => $cardData['cardCompany'] ?? '', // 카드사 코드 + 'CardType' => $cardData['cardType'] ?? '1', // 카드 종류 (1: 개인, 2: 법인) + 'CardNum' => $cardData['cardNum'] ?? '', // 카드번호 + 'WebId' => $cardData['webId'] ?? '', // 카드사 웹 ID + 'WebPwd' => $cardData['webPwd'] ?? '', // 카드사 웹 비밀번호 + 'Alias' => $cardData['alias'] ?? '', // 카드 별칭 + 'Usage' => $cardData['usage'] ?? '1' // 용도 (1: 세금계산서, 2: 기타) + ]); +} + +?> + diff --git a/ecard/api/cards.php b/ecard/api/cards.php new file mode 100644 index 0000000..b7192b0 --- /dev/null +++ b/ecard/api/cards.php @@ -0,0 +1,252 @@ +CardCompanyCode ?? $card->CardCompany ?? ''; + + // 카드 브랜드 (비자, 마스터카드 등) 추측 + $cardBrand = guessCardTypeFromNumber($card->CardNum ?? ''); + + // 카드 회사명 (신한, KB 등) + $cardCompanyName = !empty($card->CardCompanyName) + ? $card->CardCompanyName + : getCardCompanyName($cardCompanyCode); + + $cards[] = [ + 'cardNum' => $card->CardNum ?? '', + 'cardNumMasked' => maskCardNumber($card->CardNum ?? ''), + 'cardCompany' => $cardCompanyCode, + 'cardCompanyName' => $cardCompanyName, + 'cardBrand' => $cardBrand, // 카드 브랜드 (비자, 마스터카드 등) + 'alias' => $card->Alias ?? '', + 'cardType' => $card->CardType ?? '', + 'cardTypeName' => getCardTypeName($card->CardType ?? ''), + 'status' => $card->Status ?? '', + 'statusName' => getCardStatusName($card->Status ?? ''), + 'collectCycle' => $card->CollectCycle ?? '', + 'collectCycleName' => getCollectCycleName($card->CollectCycle ?? ''), + 'lastCollectDate' => formatDate($card->LastCollectDate ?? ''), + 'lastCollectResult' => $card->LastCollectResult ?? '', + 'lastCollectResultName' => getCollectResultName($card->LastCollectResult ?? ''), + 'nextExtendDate' => formatDate($card->NextExtendDate ?? ''), + 'registDate' => formatDate($card->RegistDate ?? ''), + 'webId' => $card->WebId ?? '' + ]; + } + + echo json_encode([ + 'success' => true, + 'cards' => $cards, + 'count' => count($cards) + ], JSON_UNESCAPED_UNICODE); + } else { + echo json_encode([ + 'success' => false, + 'error' => $result['error'], + 'error_code' => $result['error_code'] ?? null + ], JSON_UNESCAPED_UNICODE); + } +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} + +/** + * 카드번호 마스킹 + */ +function maskCardNumber($cardNum) { + if (strlen($cardNum) < 8) return $cardNum; + return substr($cardNum, 0, 4) . '-****-****-' . substr($cardNum, -4); +} + +/** + * 날짜 포맷팅 + */ +function formatDate($date) { + if (empty($date)) return ''; + if (strlen($date) === 8) { + return substr($date, 0, 4) . '-' . substr($date, 4, 2) . '-' . substr($date, 6, 2); + } + return $date; +} + +/** + * 카드번호로 카드 종류 추측 (BIN 코드 기반) + */ +function guessCardTypeFromNumber($cardNum) { + if (empty($cardNum) || strlen($cardNum) < 4) { + return '카드'; + } + + $bin = substr($cardNum, 0, 4); + + // 주요 카드사 BIN 코드 + $binMappings = [ + '4518' => '비자', + '4092' => '비자', + '4569' => '비자', + '4563' => '비자', + '5' => '마스터카드', // 5로 시작 + '3528' => 'JCB', + '3529' => 'JCB', + '3' => '아멕스/다이너스', // 34, 37로 시작 + '9' => '국내전용카드' + ]; + + // 정확한 매칭 시도 + if (isset($binMappings[$bin])) { + return $binMappings[$bin]; + } + + // 첫 번째 숫자로 매칭 시도 + $firstDigit = substr($cardNum, 0, 1); + if (isset($binMappings[$firstDigit])) { + return $binMappings[$firstDigit]; + } + + return '카드'; +} + +/** + * 카드사 코드 -> 이름 변환 + * 바로빌 카드사 코드 참고 + */ +function getCardCompanyName($code) { + $companies = [ + '01' => '비씨카드', + '02' => 'KB국민카드', + '03' => '하나카드(외환)', + '04' => '삼성카드', + '06' => '신한카드', + '07' => '현대카드', + '08' => '롯데카드', + '11' => 'NH농협카드', + '12' => '수협카드', + '13' => '씨티카드', + '14' => '우리카드', + '15' => '광주카드', + '16' => '전북카드', + '21' => '하나카드', + '22' => '제주카드', + '23' => 'SC제일카드', + '25' => 'KDB산업카드', + '26' => 'IBK기업카드', + '27' => '새마을금고', + '28' => '신협카드', + '29' => '저축은행', + '30' => '우체국카드', + '31' => '카카오뱅크', + '32' => 'K뱅크', + '33' => '토스뱅크', + 'BC' => '비씨카드', + 'KB' => 'KB국민카드', + 'HANA' => '하나카드', + 'SAMSUNG' => '삼성카드', + 'SHINHAN' => '신한카드', + 'HYUNDAI' => '현대카드', + 'LOTTE' => '롯데카드', + 'NH' => 'NH농협카드', + 'SUHYUP' => '수협카드', + 'CITI' => '씨티카드', + 'WOORI' => '우리카드', + 'KJBANK' => '광주카드', + 'JBBANK' => '전북카드' + ]; + return $companies[$code] ?? $code; +} + +/** + * 카드 종류 코드 -> 이름 변환 + */ +function getCardTypeName($type) { + $types = [ + '1' => '개인카드', + '2' => '법인카드' + ]; + return $types[$type] ?? $type; +} + +/** + * 카드 상태 코드 -> 이름 변환 + */ +function getCardStatusName($status) { + $statuses = [ + '0' => '대기중', + '1' => '정상', + '2' => '해지', + '3' => '수집오류', + '4' => '일시중지' + ]; + return $statuses[$status] ?? $status; +} + +/** + * 수집주기 코드 -> 이름 변환 + */ +function getCollectCycleName($cycle) { + $cycles = [ + '1' => '1일 1회', + '2' => '1일 2회', + '3' => '1일 3회' + ]; + return $cycles[$cycle] ?? $cycle; +} + +/** + * 수집결과 코드 -> 이름 변환 + */ +function getCollectResultName($result) { + $results = [ + '0' => '대기', + '1' => '성공', + '2' => '실패', + '3' => '진행중' + ]; + return $results[$result] ?? $result; +} +?> + diff --git a/ecard/api/debug_raw.php b/ecard/api/debug_raw.php new file mode 100644 index 0000000..dff0dcd --- /dev/null +++ b/ecard/api/debug_raw.php @@ -0,0 +1,65 @@ + $barobillCertKey, + 'CorpNum' => $barobillCorpNum, + 'ID' => '', + 'CardNum' => '', + 'StartDate' => $startDate, + 'EndDate' => $endDate, + 'CountPerPage' => 10, + 'CurrentPage' => 1, + 'OrderDirection' => 2 + ]; + + $result = $barobillCardSoapClient->GetPeriodCardApprovalLog($params); + $resultData = $result->GetPeriodCardApprovalLogResult; + + // raw 데이터 출력 + $output = [ + 'params' => $params, + 'resultKeys' => is_object($resultData) ? array_keys(get_object_vars($resultData)) : 'not object', + 'CurrentPage' => $resultData->CurrentPage ?? null, + 'MaxIndex' => $resultData->MaxIndex ?? null + ]; + + // CardLogList 확인 + if (isset($resultData->CardLogList)) { + $output['CardLogListKeys'] = is_object($resultData->CardLogList) ? array_keys(get_object_vars($resultData->CardLogList)) : 'not object'; + + if (isset($resultData->CardLogList->CardApprovalLog)) { + $logs = $resultData->CardLogList->CardApprovalLog; + if (!is_array($logs)) { + $logs = [$logs]; + } + + if (!empty($logs)) { + $firstLog = $logs[0]; + $output['firstLogKeys'] = is_object($firstLog) ? array_keys(get_object_vars($firstLog)) : 'not object'; + $output['firstLogData'] = $firstLog; + } + } + } + + echo json_encode($output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + +} catch (Exception $e) { + echo json_encode([ + 'error' => $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} +?> + diff --git a/ecard/api/usage.php b/ecard/api/usage.php new file mode 100644 index 0000000..e296800 --- /dev/null +++ b/ecard/api/usage.php @@ -0,0 +1,402 @@ + true, + 'data' => [ + 'currentPage' => 1, + 'countPerPage' => $limit, + 'maxPageNum' => 1, + 'maxIndex' => 0, + 'logs' => [] + ] + ]; + } else { + // 각 카드별로 조회 후 병합 + $allLogs = []; + foreach ($cardsResult['data'] as $card) { + $cardNumToQuery = $card->CardNum ?? ''; + if (empty($cardNumToQuery)) continue; + + switch ($type) { + case 'daily': + $baseDate = $_GET['baseDate'] ?? date('Ymd'); + $tempResult = getDailyCardUsage($cardNumToQuery, $baseDate, 100, 1, $orderDirection); + break; + case 'monthly': + $baseMonth = $_GET['baseMonth'] ?? date('Ym'); + $tempResult = getMonthlyCardUsage($cardNumToQuery, $baseMonth, 100, 1, $orderDirection); + break; + case 'period': + default: + $startDate = $_GET['startDate'] ?? date('Ymd', strtotime('-30 days')); + $endDate = $_GET['endDate'] ?? date('Ymd'); + $tempResult = getPeriodCardUsage($cardNumToQuery, $startDate, $endDate, 100, 1, $orderDirection); + break; + } + + if ($tempResult['success'] && !empty($tempResult['data']['logs'])) { + $allLogs = array_merge($allLogs, $tempResult['data']['logs']); + } + } + + // UseDT 기준으로 정렬 + usort($allLogs, function($a, $b) use ($orderDirection) { + $aTime = $a->UseDT ?? ''; + $bTime = $b->UseDT ?? ''; + return $orderDirection == 1 ? strcmp($aTime, $bTime) : strcmp($bTime, $aTime); + }); + + // 페이징 처리 + $totalCount = count($allLogs); + $maxPageNum = ceil($totalCount / $limit); + $offset = ($page - 1) * $limit; + $pagedLogs = array_slice($allLogs, $offset, $limit); + + $result = [ + 'success' => true, + 'data' => [ + 'currentPage' => $page, + 'countPerPage' => $limit, + 'maxPageNum' => $maxPageNum, + 'maxIndex' => $totalCount, + 'logs' => $pagedLogs + ] + ]; + } + } else { + // 특정 카드 조회 + switch ($type) { + case 'daily': + $baseDate = $_GET['baseDate'] ?? date('Ymd'); + $result = getDailyCardUsage($cardNum, $baseDate, $limit, $page, $orderDirection); + break; + + case 'monthly': + $baseMonth = $_GET['baseMonth'] ?? date('Ym'); + $result = getMonthlyCardUsage($cardNum, $baseMonth, $limit, $page, $orderDirection); + break; + + case 'period': + default: + $startDate = $_GET['startDate'] ?? date('Ymd', strtotime('-30 days')); + $endDate = $_GET['endDate'] ?? date('Ymd'); + $result = getPeriodCardUsage($cardNum, $startDate, $endDate, $limit, $page, $orderDirection); + break; + } + } + + if ($result['success']) { + $logs = []; + + // 디버그: raw 로그 데이터 출력 + if ($debugMode && !empty($result['data']['logs'])) { + $firstLog = $result['data']['logs'][0]; + error_log('CardApprovalLog raw data: ' . print_r($firstLog, true)); + // 디버그: 모든 필드명 확인 + if (is_object($firstLog)) { + $fields = get_object_vars($firstLog); + error_log('CardApprovalLog fields: ' . implode(', ', array_keys($fields))); + error_log('ApprovalAmount value: ' . ($firstLog->ApprovalAmount ?? 'NOT SET')); + error_log('Amount value: ' . ($firstLog->Amount ?? 'NOT SET')); + error_log('TotalAmount value: ' . ($firstLog->TotalAmount ?? 'NOT SET')); + } + } + + foreach ($result['data']['logs'] as $log) { + // UseDT 형식: YYYYMMDDHHMMSS + $useDT = $log->UseDT ?? ''; + $approvalDate = ''; + $approvalTime = ''; + if (strlen($useDT) >= 8) { + $approvalDate = substr($useDT, 0, 4) . '-' . substr($useDT, 4, 2) . '-' . substr($useDT, 6, 2); + } + if (strlen($useDT) >= 14) { + $approvalTime = substr($useDT, 8, 2) . ':' . substr($useDT, 10, 2) . ':' . substr($useDT, 12, 2); + } elseif (strlen($useDT) >= 12) { + $approvalTime = substr($useDT, 8, 2) . ':' . substr($useDT, 10, 2); + } + + $logs[] = [ + 'cardNum' => maskCardNumber($log->CardNum ?? ''), + 'cardNumFull' => $log->CardNum ?? '', + 'approvalNum' => $log->ApprovalNum ?? '', + 'approvalDate' => $approvalDate, + 'approvalTime' => $approvalTime, + 'approvalDateTime' => $approvalDate . ' ' . $approvalTime, + 'merchantName' => $log->UseStoreName ?? '', + 'merchantBizNum' => $log->UseStoreCorpNum ?? '', + // 금액 필드: 여러 가능한 필드명 시도 + // ApprovalAmount가 실제 승인금액 (화면에 표시할 금액) + 'amount' => intval($log->ApprovalAmount ?? 0), + 'amountFormatted' => number_format(intval($log->ApprovalAmount ?? 0)), + 'vat' => intval($log->Tax ?? 0), + 'vatFormatted' => number_format(intval($log->Tax ?? 0)), + 'serviceCharge' => intval($log->ServiceCharge ?? 0), + // totalAmount는 화면에서 사용하므로 ApprovalAmount를 사용 + 'totalAmount' => intval($log->ApprovalAmount ?? 0), + 'totalAmountFormatted' => number_format(intval($log->ApprovalAmount ?? 0)), + 'approvalType' => $log->ApprovalType ?? '', + 'approvalTypeName' => getApprovalTypeName($log->ApprovalType ?? ''), + 'installment' => $log->PaymentPlan ?? '', + 'installmentName' => getInstallmentName($log->PaymentPlan ?? ''), + 'currencyCode' => $log->CurrencyCode ?? 'KRW', + 'memo' => $log->Memo ?? '', + 'cardCompany' => $log->CardCompany ?? '', + 'cardCompanyName' => getCardCompanyNameFromLog($log->CardCompany ?? ''), + // 추가 필드 + 'useKey' => $log->UseKey ?? '', + 'storeAddress' => $log->UseStoreAddr ?? '', + 'storeCeo' => $log->UseStoreCeo ?? '', + 'storeBizType' => $log->UseStoreBizType ?? '', + 'storeTel' => $log->UseStoreTel ?? '' + ]; + } + + // 통계 계산 + $totalAmount = array_sum(array_column($logs, 'totalAmount')); + $approvalCount = count(array_filter($logs, function($l) { return $l['approvalType'] == '1'; })); + $cancelCount = count(array_filter($logs, function($l) { return $l['approvalType'] == '2'; })); + + $response = [ + 'success' => true, + 'data' => [ + 'logs' => $logs, + 'pagination' => [ + 'currentPage' => $result['data']['currentPage'], + 'countPerPage' => $result['data']['countPerPage'], + 'maxPageNum' => $result['data']['maxPageNum'], + 'totalCount' => $result['data']['maxIndex'] + ], + 'summary' => [ + 'totalAmount' => $totalAmount, + 'totalAmountFormatted' => number_format($totalAmount), + 'count' => count($logs), + 'approvalCount' => $approvalCount, + 'cancelCount' => $cancelCount + ] + ] + ]; + + // 디버그 정보 추가 (성공한 경우에도 항상 첫 번째 로그의 필드 정보 출력) + if (!empty($result['data']['logs'])) { + $firstLog = $result['data']['logs'][0]; + if (is_object($firstLog)) { + // 모든 필드를 배열로 변환 + $allFields = get_object_vars($firstLog); + $fieldNames = array_keys($allFields); + + // 금액 관련 필드 찾기 (대소문자 구분 없이) + $amountFields = []; + foreach ($fieldNames as $fieldName) { + if (stripos($fieldName, 'amount') !== false || + stripos($fieldName, 'cost') !== false || + stripos($fieldName, 'price') !== false || + stripos($fieldName, '금액') !== false) { + $amountFields[$fieldName] = (string)($firstLog->$fieldName ?? 'NULL'); + } + } + + // 디버그 모드일 때만 상세 정보 출력 + if ($debugMode) { + $response['debug'] = [ + 'userId' => getBarobillUserId(), + 'params' => $_GET, + 'firstLogFields' => $fieldNames, + 'firstLogAllValues' => array_map(function($v) { + return is_string($v) ? $v : (is_numeric($v) ? (string)$v : gettype($v)); + }, $allFields), + 'amountFields' => $amountFields + ]; + } else { + // 디버그 모드가 아니어도 금액 필드 정보는 항상 포함 (문제 해결용) + $response['debug'] = [ + 'amountFields' => $amountFields, + 'allFields' => $fieldNames + ]; + } + } + } elseif ($debugMode) { + $response['debug'] = [ + 'userId' => getBarobillUserId(), + 'params' => $_GET, + 'message' => 'No logs found' + ]; + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + } else { + $response = [ + 'success' => false, + 'error' => $result['error'], + 'error_code' => $result['error_code'] ?? null + ]; + + // 디버그 정보 추가 + if ($debugMode) { + $response['debug'] = [ + 'userId' => getBarobillUserId(), + 'params' => $_GET + ]; + } + + echo json_encode($response, JSON_UNESCAPED_UNICODE); + } +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} + +/** + * 카드번호 마스킹 + */ +function maskCardNumber($cardNum) { + if (strlen($cardNum) < 8) return $cardNum; + return substr($cardNum, 0, 4) . '-****-****-' . substr($cardNum, -4); +} + +/** + * 날짜 포맷팅 + */ +function formatDate($date) { + if (strlen($date) === 8) { + return substr($date, 0, 4) . '-' . substr($date, 4, 2) . '-' . substr($date, 6, 2); + } + return $date; +} + +/** + * 시간 포맷팅 + */ +function formatTime($time) { + if (strlen($time) === 6) { + return substr($time, 0, 2) . ':' . substr($time, 2, 2) . ':' . substr($time, 4, 2); + } elseif (strlen($time) === 4) { + return substr($time, 0, 2) . ':' . substr($time, 2, 2); + } + return $time; +} + +/** + * 승인 유형 이름 + */ +function getApprovalTypeName($type) { + $types = [ + '1' => '승인', + '2' => '취소' + ]; + return $types[$type] ?? $type; +} + +/** + * 할부 이름 + */ +function getInstallmentName($installment) { + if (empty($installment) || $installment == '0' || $installment == '00') { + return '일시불'; + } + return $installment . '개월'; +} + +/** + * 카드사 이름 (로그용) + */ +function getCardCompanyNameFromLog($code) { + $companies = [ + '01' => '비씨', + '02' => 'KB국민', + '03' => '하나(외환)', + '04' => '삼성', + '06' => '신한', + '07' => '현대', + '08' => '롯데', + '11' => 'NH농협', + '12' => '수협', + '13' => '씨티', + '14' => '우리', + '15' => '광주', + '16' => '전북', + '21' => '하나', + '22' => '제주', + '23' => 'SC제일', + '25' => 'KDB산업', + '26' => 'IBK기업', + '27' => '새마을금고', + '28' => '신협', + '29' => '저축은행', + '30' => '우체국', + '31' => '카카오뱅크', + '32' => 'K뱅크', + '33' => '토스뱅크' + ]; + return $companies[$code] ?? $code; +} +?> + diff --git a/ecard/index.php b/ecard/index.php new file mode 100644 index 0000000..f81c7af --- /dev/null +++ b/ecard/index.php @@ -0,0 +1,920 @@ + + + + + + 법인카드 사용내역 조회 - 바로빌 연동 + + + + + + + + + + + + + + + + + +
+ + + + + diff --git a/ecard/카드사용내역조회.md b/ecard/카드사용내역조회.md new file mode 100644 index 0000000..b7cf466 --- /dev/null +++ b/ecard/카드사용내역조회.md @@ -0,0 +1,957 @@ +# 바로빌 카드 사용내역 조회 - 멀티테넌시 개발 문서 + +## 목차 +1. [개요](#개요) +2. [시스템 아키텍처](#시스템-아키텍처) +3. [데이터베이스 설계](#데이터베이스-설계) +4. [API 구조](#api-구조) +5. [보안 고려사항](#보안-고려사항) +6. [구현 가이드](#구현-가이드) + +--- + +## 개요 + +### 목적 +멀티테넌시 환경에서 각 업체(테넌트)별로 독립적인 바로빌 카드 사용내역 조회 서비스를 제공하기 위한 개발 문서입니다. + +### 주요 기능 +- 업체별 바로빌 인증 정보 관리 +- 업체별 카드 목록 조회 +- 업체별 카드 사용내역 조회 +- 데이터 격리 및 보안 관리 + +### 기술 스택 +- **백엔드**: PHP 7.3+ +- **데이터베이스**: MySQL/MariaDB +- **외부 API**: 바로빌 SOAP 웹서비스 +- **프론트엔드**: React (ecard/index.php) + +--- + +## 시스템 아키텍처 + +### 데이터 흐름도 + +``` +[업체 사용자] + ↓ +[ecard/index.php] (프론트엔드) + ↓ (업체 ID 전달) +[API Layer] + ├─ cards.php (카드 목록 조회) + └─ usage.php (사용내역 조회) + ↓ (업체별 인증 정보 조회) +[Database] + ├─ companies (업체 정보) + └─ barobill_credentials (바로빌 인증 정보) + ↓ (바로빌 API 호출) +[바로빌 SOAP API] + ├─ GetCardEx2 (카드 목록) + └─ GetPeriodCardApprovalLog (사용내역) + ↓ (응답) +[API Layer] → [프론트엔드] → [사용자] +``` + +### 멀티테넌시 구조 + +``` +┌─────────────────────────────────────────┐ +│ 업체 A (Company A) │ +│ ┌───────────────────────────────────┐ │ +│ │ 바로빌 인증 정보 │ │ +│ │ - CERTKEY: AAAAA │ │ +│ │ - 사업자번호: 123-45-67890 │ │ +│ │ - 사용자 ID: user_a │ │ +│ └───────────────────────────────────┘ │ +│ ┌───────────────────────────────────┐ │ +│ │ 카드 목록 (바로빌에서 조회) │ │ +│ │ - 카드1, 카드2, 카드3 │ │ +│ └───────────────────────────────────┘ │ +└─────────────────────────────────────────┘ + +┌─────────────────────────────────────────┐ +│ 업체 B (Company B) │ +│ ┌───────────────────────────────────┐ │ +│ │ 바로빌 인증 정보 │ │ +│ │ - CERTKEY: BBBBB │ │ +│ │ - 사업자번호: 987-65-43210 │ │ +│ │ - 사용자 ID: user_b │ │ +│ └───────────────────────────────────┘ │ +│ ┌───────────────────────────────────┐ │ +│ │ 카드 목록 (바로빌에서 조회) │ │ +│ │ - 카드4, 카드5 │ │ +│ └───────────────────────────────────┘ │ +└─────────────────────────────────────────┘ +``` + +--- + +## 데이터베이스 설계 + +### 1. companies (업체 기본 정보 테이블) + +업체의 기본 정보를 저장하는 테이블입니다. + +```sql +CREATE TABLE companies ( + id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '업체 ID', + company_name VARCHAR(255) NOT NULL COMMENT '업체명', + business_number VARCHAR(20) NOT NULL COMMENT '사업자번호 (하이픈 포함)', + business_number_clean VARCHAR(20) NOT NULL COMMENT '사업자번호 (하이픈 제거)', + status ENUM('active', 'inactive', 'suspended') DEFAULT 'active' COMMENT '상태', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시', + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시', + deleted_at DATETIME NULL COMMENT '삭제일시 (소프트 삭제)', + + UNIQUE KEY uk_business_number (business_number_clean), + INDEX idx_status (status), + INDEX idx_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='업체 기본 정보'; +``` + +**주요 필드 설명:** +- `id`: 업체 고유 ID (다른 테이블에서 외래키로 사용) +- `company_name`: 업체명 +- `business_number`: 사업자번호 (표시용, 하이픈 포함) +- `business_number_clean`: 사업자번호 (검색용, 하이픈 제거) +- `status`: 업체 상태 (active=활성, inactive=비활성, suspended=정지) + +--- + +### 2. barobill_credentials (바로빌 인증 정보 테이블) + +각 업체별 바로빌 API 인증 정보를 저장하는 테이블입니다. + +```sql +CREATE TABLE barobill_credentials ( + id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '인증 정보 ID', + company_id INT(11) UNSIGNED NOT NULL COMMENT '업체 ID', + cert_key VARCHAR(500) NOT NULL COMMENT '바로빌 CERTKEY (암호화 권장)', + corp_num VARCHAR(20) NOT NULL COMMENT '사업자번호 (하이픈 제거)', + user_id VARCHAR(100) NULL COMMENT '바로빌 사용자 ID (선택사항, 빈값이면 전체 카드 조회)', + test_mode TINYINT(1) DEFAULT 0 COMMENT '테스트 모드 (0=운영, 1=테스트)', + status ENUM('active', 'inactive') DEFAULT 'active' COMMENT '상태', + last_api_call DATETIME NULL COMMENT '마지막 API 호출 일시', + last_error_message TEXT NULL COMMENT '마지막 에러 메시지', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시', + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시', + + UNIQUE KEY uk_company_id (company_id), + INDEX idx_status (status), + INDEX idx_company_id (company_id), + + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='바로빌 인증 정보'; +``` + +**주요 필드 설명:** +- `company_id`: 업체 ID (companies 테이블 참조) +- `cert_key`: 바로빌 CERTKEY (⚠️ 민감정보, 암호화 권장) +- `corp_num`: 사업자번호 (하이픈 제거) +- `user_id`: 바로빌 사용자 ID (특정 사용자 카드만 조회 시 사용, NULL이면 전체) +- `test_mode`: 테스트 모드 여부 (0=운영, 1=테스트) +- `status`: 인증 정보 상태 +- `last_api_call`: 마지막 API 호출 일시 (모니터링용) +- `last_error_message`: 마지막 에러 메시지 (디버깅용) + +**보안 고려사항:** +- `cert_key`는 민감정보이므로 암호화 저장 권장 +- 데이터베이스 접근 권한 최소화 +- 로그에 민감정보 출력 금지 + +--- + +### 3. barobill_cards (카드 정보 캐시 테이블) + +바로빌에서 조회한 카드 정보를 캐싱하는 테이블입니다. (선택사항) + +```sql +CREATE TABLE barobill_cards ( + id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '카드 ID', + company_id INT(11) UNSIGNED NOT NULL COMMENT '업체 ID', + card_num VARCHAR(50) NOT NULL COMMENT '카드번호', + card_company_code VARCHAR(10) NULL COMMENT '카드사 코드', + card_company_name VARCHAR(50) NULL COMMENT '카드사 이름', + card_brand VARCHAR(20) NULL COMMENT '카드 브랜드 (비자, 마스터카드 등)', + alias VARCHAR(100) NULL COMMENT '카드 별칭', + card_type TINYINT(1) NULL COMMENT '카드 종류 (1=개인, 2=법인)', + status TINYINT(1) NULL COMMENT '카드 상태 (0=대기중, 1=정상, 2=해지, 3=수집오류, 4=일시중지)', + collect_cycle TINYINT(1) NULL COMMENT '수집주기 (1=1일1회, 2=1일2회, 3=1일3회)', + last_collect_date DATE NULL COMMENT '마지막 수집일', + last_collect_result TINYINT(1) NULL COMMENT '마지막 수집결과', + regist_date DATE NULL COMMENT '등록일', + cached_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '캐시 일시', + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일시', + + UNIQUE KEY uk_company_card (company_id, card_num), + INDEX idx_company_id (company_id), + INDEX idx_status (status), + INDEX idx_cached_at (cached_at), + + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='바로빌 카드 정보 캐시'; +``` + +**주요 필드 설명:** +- `company_id`: 업체 ID +- `card_num`: 카드번호 (바로빌에서 조회한 카드번호) +- `card_company_code`: 카드사 코드 (01=BC, 02=KB, 04=삼성 등) +- `card_company_name`: 카드사 이름 +- `card_brand`: 카드 브랜드 (비자, 마스터카드 등) +- `alias`: 카드 별칭 +- `status`: 카드 상태 +- `cached_at`: 캐시 일시 (캐시 만료 판단용) + +**사용 목적:** +- 바로빌 API 호출 최소화 (성능 향상) +- 오프라인 조회 가능 +- 카드 목록 변경 이력 추적 + +**캐시 전략:** +- 카드 목록은 1시간마다 갱신 권장 +- 실시간 조회가 필요한 경우 캐시 사용 안 함 + +--- + +### 4. barobill_card_usage_logs (카드 사용내역 캐시 테이블) + +바로빌에서 조회한 카드 사용내역을 캐싱하는 테이블입니다. (선택사항) + +```sql +CREATE TABLE barobill_card_usage_logs ( + id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '사용내역 ID', + company_id INT(11) UNSIGNED NOT NULL COMMENT '업체 ID', + card_num VARCHAR(50) NOT NULL COMMENT '카드번호', + use_dt DATETIME NOT NULL COMMENT '사용일시', + use_key VARCHAR(100) NULL COMMENT '사용 키 (바로빌 고유값)', + approval_num VARCHAR(50) NULL COMMENT '승인번호', + approval_amount INT(11) DEFAULT 0 COMMENT '승인금액', + tax INT(11) DEFAULT 0 COMMENT '부가세', + service_charge INT(11) DEFAULT 0 COMMENT '봉사료', + total_amount INT(11) DEFAULT 0 COMMENT '총 금액', + approval_type TINYINT(1) NULL COMMENT '승인유형 (1=승인, 2=취소)', + payment_plan VARCHAR(10) NULL COMMENT '할부개월 (0=일시불)', + currency_code VARCHAR(3) DEFAULT 'KRW' COMMENT '통화코드', + use_store_name VARCHAR(255) NULL COMMENT '가맹점명', + use_store_corp_num VARCHAR(20) NULL COMMENT '가맹점 사업자번호', + use_store_addr TEXT NULL COMMENT '가맹점 주소', + use_store_ceo VARCHAR(100) NULL COMMENT '가맹점 대표자명', + use_store_biz_type VARCHAR(100) NULL COMMENT '가맹점 업종', + use_store_tel VARCHAR(20) NULL COMMENT '가맹점 전화번호', + memo TEXT NULL COMMENT '메모', + card_company VARCHAR(10) NULL COMMENT '카드사 코드', + cached_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '캐시 일시', + + UNIQUE KEY uk_use_key (company_id, use_key), + INDEX idx_company_id (company_id), + INDEX idx_card_num (card_num), + INDEX idx_use_dt (use_dt), + INDEX idx_company_use_dt (company_id, use_dt), + INDEX idx_approval_type (approval_type), + INDEX idx_cached_at (cached_at), + + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='바로빌 카드 사용내역 캐시'; +``` + +**주요 필드 설명:** +- `company_id`: 업체 ID +- `card_num`: 카드번호 +- `use_dt`: 사용일시 +- `use_key`: 바로빌 고유 사용 키 (중복 방지용) +- `approval_amount`: 승인금액 +- `approval_type`: 승인유형 (1=승인, 2=취소) +- `use_store_name`: 가맹점명 +- `cached_at`: 캐시 일시 + +**인덱스 전략:** +- `idx_company_use_dt`: 업체별 기간 조회 최적화 +- `idx_use_dt`: 전체 기간 조회 최적화 +- `uk_use_key`: 중복 데이터 방지 + +**사용 목적:** +- 바로빌 API 호출 최소화 +- 빠른 조회 성능 +- 데이터 분석 및 리포트 생성 + +**캐시 전략:** +- 최근 3개월 데이터는 캐시 유지 +- 오래된 데이터는 주기적으로 정리 +- 실시간 조회가 필요한 경우 바로빌 API 직접 호출 + +--- + +### 5. barobill_api_logs (API 호출 로그 테이블) + +바로빌 API 호출 이력을 기록하는 테이블입니다. (선택사항, 모니터링용) + +```sql +CREATE TABLE barobill_api_logs ( + id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '로그 ID', + company_id INT(11) UNSIGNED NOT NULL COMMENT '업체 ID', + api_method VARCHAR(50) NOT NULL COMMENT 'API 메서드명', + request_params TEXT NULL COMMENT '요청 파라미터 (JSON)', + response_status VARCHAR(20) NULL COMMENT '응답 상태 (success/failure)', + response_data TEXT NULL COMMENT '응답 데이터 (JSON, 일부만 저장)', + error_message TEXT NULL COMMENT '에러 메시지', + execution_time INT(11) NULL COMMENT '실행 시간 (밀리초)', + ip_address VARCHAR(45) NULL COMMENT '요청 IP 주소', + user_agent VARCHAR(255) NULL COMMENT '사용자 에이전트', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '생성일시', + + INDEX idx_company_id (company_id), + INDEX idx_api_method (api_method), + INDEX idx_response_status (response_status), + INDEX idx_created_at (created_at), + INDEX idx_company_created (company_id, created_at), + + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='바로빌 API 호출 로그'; +``` + +**주요 필드 설명:** +- `company_id`: 업체 ID +- `api_method`: API 메서드명 (GetCardEx2, GetPeriodCardApprovalLog 등) +- `request_params`: 요청 파라미터 (JSON 형식) +- `response_status`: 응답 상태 (success/failure) +- `error_message`: 에러 메시지 +- `execution_time`: 실행 시간 (성능 모니터링용) + +**사용 목적:** +- API 호출 이력 추적 +- 에러 디버깅 +- 성능 모니터링 +- 사용량 통계 + +**데이터 보관 정책:** +- 최근 6개월 데이터 보관 +- 오래된 데이터는 주기적으로 아카이빙 또는 삭제 + +--- + +## 테이블 관계도 + +``` +companies (업체) + │ + ├── 1:1 ── barobill_credentials (바로빌 인증 정보) + │ + ├── 1:N ── barobill_cards (카드 정보 캐시) + │ + ├── 1:N ── barobill_card_usage_logs (사용내역 캐시) + │ + └── 1:N ── barobill_api_logs (API 호출 로그) +``` + +--- + +## API 구조 + +### 1. 카드 목록 조회 API + +**엔드포인트**: `GET /ecard/api/cards.php` + +**요청 파라미터:** +```php +[ + 'company_id' => 1, // 업체 ID (필수) + 'availOnly' => 0 // 0=전체, 1=사용가능한 카드만 +] +``` + +**응답 예시:** +```json +{ + "success": true, + "cards": [ + { + "cardNum": "1234567890123456", + "cardNumMasked": "1234-****-****-3456", + "cardCompany": "04", + "cardCompanyName": "삼성카드", + "cardBrand": "비자", + "alias": "법인카드1", + "status": "1", + "statusName": "정상" + } + ], + "count": 1 +} +``` + +**구현 로직:** +1. `company_id`로 `barobill_credentials` 테이블에서 인증 정보 조회 +2. 바로빌 SOAP API 호출 (GetCardEx2) +3. 응답 데이터 변환 및 반환 + +--- + +### 2. 카드 사용내역 조회 API + +**엔드포인트**: `GET /ecard/api/usage.php` + +**요청 파라미터:** +```php +[ + 'company_id' => 1, // 업체 ID (필수) + 'type' => 'period', // daily/monthly/period + 'cardNum' => '', // 카드번호 (빈값이면 전체) + 'startDate' => '20240101', // 시작일 (YYYYMMDD) + 'endDate' => '20240131', // 종료일 (YYYYMMDD) + 'page' => 1, // 페이지 번호 + 'limit' => 50 // 페이지당 건수 +] +``` + +**응답 예시:** +```json +{ + "success": true, + "data": { + "logs": [ + { + "cardNum": "1234-****-****-3456", + "approvalDateTime": "2024-01-15 14:30:00", + "merchantName": "스타벅스 강남점", + "merchantBizNum": "123-45-67890", + "amount": 5000, + "approvalType": "1", + "approvalTypeName": "승인" + } + ], + "pagination": { + "currentPage": 1, + "maxPageNum": 10, + "totalCount": 500 + }, + "summary": { + "totalAmount": 1000000, + "count": 500, + "approvalCount": 480, + "cancelCount": 20 + } + } +} +``` + +**구현 로직:** +1. `company_id`로 `barobill_credentials` 테이블에서 인증 정보 조회 +2. 바로빌 SOAP API 호출 (GetPeriodCardApprovalLog) +3. 응답 데이터 변환 및 반환 +4. (선택) 캐시 테이블에 저장 + +--- + +## 보안 고려사항 + +### 1. 데이터 격리 + +- **업체별 데이터 격리**: 모든 쿼리에 `company_id` 조건 필수 +- **권한 검증**: 세션에서 `company_id` 확인 후 접근 허용 +- **SQL Injection 방지**: Prepared Statement 사용 + +```php +// 올바른 예시 +$stmt = $pdo->prepare("SELECT * FROM barobill_credentials WHERE company_id = ?"); +$stmt->execute([$company_id]); + +// 잘못된 예시 (SQL Injection 취약) +$sql = "SELECT * FROM barobill_credentials WHERE company_id = $company_id"; +``` + +### 2. 인증 정보 보호 + +- **CERTKEY 암호화**: 데이터베이스에 저장 시 암호화 +- **접근 로그**: 인증 정보 조회 시 로그 기록 +- **최소 권한 원칙**: 필요한 최소한의 정보만 조회 + +```php +// CERTKEY 암호화 예시 (간단한 방법) +function encryptCertKey($certKey) { + // 실제 운영 환경에서는 더 강력한 암호화 사용 권장 + return base64_encode(openssl_encrypt($certKey, 'AES-256-CBC', $encryptionKey)); +} + +function decryptCertKey($encryptedCertKey) { + return openssl_decrypt(base64_decode($encryptedCertKey), 'AES-256-CBC', $encryptionKey); +} +``` + +### 3. API 호출 제한 + +- **Rate Limiting**: 업체별 API 호출 횟수 제한 +- **에러 처리**: 에러 발생 시 민감정보 노출 금지 +- **타임아웃 설정**: API 호출 타임아웃 설정 + +```php +// Rate Limiting 예시 +function checkRateLimit($company_id) { + $stmt = $pdo->prepare(" + SELECT COUNT(*) as count + FROM barobill_api_logs + WHERE company_id = ? + AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR) + "); + $stmt->execute([$company_id]); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($result['count'] > 1000) { // 시간당 1000회 제한 + throw new Exception('API 호출 한도 초과'); + } +} +``` + +--- + +## 구현 가이드 + +### 1. barobill_card_config.php 수정 + +멀티테넌시를 지원하도록 수정합니다. + +```php +prepare(" + SELECT + bc.cert_key, + bc.corp_num, + bc.user_id, + bc.test_mode, + bc.status + FROM {$DB}.barobill_credentials bc + WHERE bc.company_id = ? + AND bc.status = 'active' + "); + $stmt->execute([$company_id]); + $credentials = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!$credentials) { + return [ + 'success' => false, + 'error' => '바로빌 인증 정보가 등록되지 않았습니다.' + ]; + } + + // CERTKEY 복호화 (암호화된 경우) + if (function_exists('decryptCertKey')) { + $credentials['cert_key'] = decryptCertKey($credentials['cert_key']); + } + + return [ + 'success' => true, + 'data' => $credentials + ]; +} + +/** + * 바로빌 카드 SOAP 웹서비스 호출 함수 (멀티테넌시 지원) + * + * @param int $company_id 업체 ID + * @param string $method SOAP 메서드명 + * @param array $params SOAP 메서드 파라미터 + * @return array 응답 데이터 + */ +function callBarobillCardSOAPForCompany($company_id, $method, $params = []) { + // 인증 정보 조회 + $credentials = getBarobillCredentials($company_id); + if (!$credentials['success']) { + return $credentials; + } + + $certKey = $credentials['data']['cert_key']; + $corpNum = $credentials['data']['corp_num']; + $isTestMode = $credentials['data']['test_mode'] == 1; + + // SOAP URL 설정 + $soapUrl = $isTestMode + ? 'https://testws.baroservice.com/CARD.asmx?WSDL' + : 'https://ws.baroservice.com/CARD.asmx?WSDL'; + + // SOAP 클라이언트 생성 + try { + $soapClient = new SoapClient($soapUrl, [ + 'trace' => true, + 'encoding' => 'UTF-8', + 'exceptions' => true, + 'connection_timeout' => 30 + ]); + } catch (Exception $e) { + return [ + 'success' => false, + 'error' => 'SOAP 클라이언트 생성 실패: ' . $e->getMessage() + ]; + } + + // CERTKEY와 CorpNum 자동 추가 + if (!isset($params['CERTKEY'])) { + $params['CERTKEY'] = $certKey; + } + if (!isset($params['CorpNum'])) { + $params['CorpNum'] = $corpNum; + } + + // API 호출 로그 기록 + $startTime = microtime(true); + + try { + $result = $soapClient->$method($params); + + $executionTime = (microtime(true) - $startTime) * 1000; // 밀리초 + + // API 호출 로그 저장 + logBarobillApiCall($company_id, $method, $params, 'success', null, $executionTime); + + $resultProperty = $method . 'Result'; + if (isset($result->$resultProperty)) { + $resultData = $result->$resultProperty; + + // 에러 코드 체크 + if (is_numeric($resultData) && $resultData < 0) { + logBarobillApiCall($company_id, $method, $params, 'failure', '에러 코드: ' . $resultData, $executionTime); + return [ + 'success' => false, + 'error' => '바로빌 카드 API 오류 코드: ' . $resultData, + 'error_code' => $resultData + ]; + } + + return [ + 'success' => true, + 'data' => $resultData + ]; + } + + return [ + 'success' => true, + 'data' => $result + ]; + + } catch (SoapFault $e) { + $executionTime = (microtime(true) - $startTime) * 1000; + logBarobillApiCall($company_id, $method, $params, 'failure', $e->getMessage(), $executionTime); + + return [ + 'success' => false, + 'error' => 'SOAP 오류: ' . $e->getMessage(), + 'error_code' => $e->getCode() + ]; + } catch (Exception $e) { + $executionTime = (microtime(true) - $startTime) * 1000; + logBarobillApiCall($company_id, $method, $params, 'failure', $e->getMessage(), $executionTime); + + return [ + 'success' => false, + 'error' => 'API 호출 오류: ' . $e->getMessage() + ]; + } +} + +/** + * API 호출 로그 저장 + */ +function logBarobillApiCall($company_id, $method, $params, $status, $error_message = null, $execution_time = null) { + global $pdo, $DB; + + try { + $stmt = $pdo->prepare(" + INSERT INTO {$DB}.barobill_api_logs + (company_id, api_method, request_params, response_status, error_message, execution_time, ip_address, user_agent) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + "); + + $stmt->execute([ + $company_id, + $method, + json_encode($params, JSON_UNESCAPED_UNICODE), + $status, + $error_message, + $execution_time, + $_SERVER['REMOTE_ADDR'] ?? null, + $_SERVER['HTTP_USER_AGENT'] ?? null + ]); + } catch (Exception $e) { + // 로그 저장 실패는 무시 (시스템 오류 방지) + error_log('API 로그 저장 실패: ' . $e->getMessage()); + } +} + +/** + * 업체별 카드 목록 조회 (멀티테넌시 지원) + */ +function getCardListForCompany($company_id, $availOnly = 0) { + $result = callBarobillCardSOAPForCompany($company_id, 'GetCardEx2', [ + 'AvailOnly' => $availOnly + ]); + + if (!$result['success']) { + return $result; + } + + $cards = []; + $data = $result['data']; + + if (!isset($data->CardEx)) { + return ['success' => true, 'data' => []]; + } + + if (!is_array($data->CardEx)) { + $cards = [$data->CardEx]; + } else { + $cards = $data->CardEx; + } + + // 에러 체크 + if (count($cards) == 1 && isset($cards[0]->CardNum) && $cards[0]->CardNum < 0) { + return [ + 'success' => false, + 'error' => '카드 목록 조회 실패', + 'error_code' => $cards[0]->CardNum + ]; + } + + // (선택) 캐시 테이블에 저장 + // saveCardsToCache($company_id, $cards); + + return ['success' => true, 'data' => $cards]; +} + +/** + * 업체별 기간별 카드 사용내역 조회 (멀티테넌시 지원) + */ +function getPeriodCardUsageForCompany($company_id, $cardNum = '', $startDate = '', $endDate = '', $countPerPage = 50, $currentPage = 1, $orderDirection = 2, $userId = '') { + // 인증 정보 조회 + $credentials = getBarobillCredentials($company_id); + if (!$credentials['success']) { + return $credentials; + } + + $barobillUserId = $credentials['data']['user_id'] ?? ''; + if (!empty($userId)) { + $barobillUserId = $userId; + } + + $result = callBarobillCardSOAPForCompany($company_id, 'GetPeriodCardApprovalLog', [ + 'ID' => $barobillUserId, + 'CardNum' => $cardNum, + 'StartDate' => $startDate, + 'EndDate' => $endDate, + 'CountPerPage' => $countPerPage, + 'CurrentPage' => $currentPage, + 'OrderDirection' => $orderDirection + ]); + + if (!$result['success']) { + return $result; + } + + return parseCardUsageResult($result['data']); +} + +// 기존 parseCardUsageResult 함수는 그대로 사용 +// ... (기존 코드 유지) +?> +``` + +### 2. cards.php 수정 + +멀티테넌시를 지원하도록 수정합니다. + +```php + false, + 'error' => '업체 ID가 필요합니다.' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + $availOnly = isset($_GET['availOnly']) ? intval($_GET['availOnly']) : 0; + + $result = getCardListForCompany($company_id, $availOnly); + + if ($result['success']) { + $cards = []; + foreach ($result['data'] as $card) { + // ... (기존 변환 로직) + } + + echo json_encode([ + 'success' => true, + 'cards' => $cards, + 'count' => count($cards) + ], JSON_UNESCAPED_UNICODE); + } else { + echo json_encode([ + 'success' => false, + 'error' => $result['error'], + 'error_code' => $result['error_code'] ?? null + ], JSON_UNESCAPED_UNICODE); + } +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} +?> +``` + +### 3. usage.php 수정 + +멀티테넌시를 지원하도록 수정합니다. + +```php + false, + 'error' => '업체 ID가 필요합니다.' + ], JSON_UNESCAPED_UNICODE); + exit; + } + + $type = $_GET['type'] ?? 'period'; + $cardNum = $_GET['cardNum'] ?? ''; + $page = max(1, intval($_GET['page'] ?? 1)); + $limit = min(100, max(10, intval($_GET['limit'] ?? 50))); + $orderDirection = intval($_GET['order'] ?? 2); + + // ... (기존 로직을 getPeriodCardUsageForCompany로 변경) + + $result = getPeriodCardUsageForCompany( + $company_id, + $cardNum, + $startDate, + $endDate, + $limit, + $page, + $orderDirection + ); + + // ... (기존 응답 로직) + +} catch (Exception $e) { + echo json_encode([ + 'success' => false, + 'error' => '서버 오류: ' . $e->getMessage() + ], JSON_UNESCAPED_UNICODE); +} +?> +``` + +--- + +## 마이그레이션 가이드 + +### 기존 단일 테넌트에서 멀티테넌트로 전환 + +1. **데이터베이스 마이그레이션** + ```sql + -- 1. companies 테이블 생성 + -- 2. barobill_credentials 테이블 생성 + -- 3. 기존 파일 기반 설정을 DB로 마이그레이션 + + INSERT INTO companies (company_name, business_number, business_number_clean, status) + VALUES ('기본 업체', '123-45-67890', '1234567890', 'active'); + + INSERT INTO barobill_credentials (company_id, cert_key, corp_num, user_id, test_mode, status) + VALUES ( + 1, + (SELECT cert_key FROM file), -- 파일에서 읽은 CERTKEY + (SELECT corp_num FROM file), -- 파일에서 읽은 사업자번호 + NULL, + 0, + 'active' + ); + ``` + +2. **코드 수정** + - `barobill_card_config.php`: 파일 기반 → DB 기반으로 변경 + - `cards.php`, `usage.php`: `company_id` 파라미터 추가 + - 세션에 `company_id` 저장 + +3. **테스트** + - 각 업체별로 독립적인 카드 조회 확인 + - 데이터 격리 확인 + - 권한 검증 확인 + +--- + +## 모니터링 및 유지보수 + +### 1. 주요 모니터링 지표 + +- API 호출 성공률 +- API 호출 응답 시간 +- 에러 발생 빈도 +- 캐시 적중률 (캐시 사용 시) + +### 2. 정기 점검 사항 + +- 인증 정보 만료 확인 +- 캐시 데이터 정리 +- API 로그 분석 +- 성능 최적화 + +--- + +## 참고 자료 + +- [바로빌 개발자 문서](https://dev.barobill.co.kr/) +- [바로빌 카드 API 레퍼런스](https://dev.barobill.co.kr/docs/references/카드조회-API) +- PHP SOAP 클라이언트 문서 + +--- + +## 변경 이력 + +| 버전 | 날짜 | 변경 내용 | 작성자 | +|------|------|----------|--------| +| 1.0 | 2025-12-08 | 초기 문서 작성 | - | + +--- + +**문서 작성일**: 2025년 12월 +**최종 수정일**: 2025년 12월 +**문서 버전**: 1.0 \ No newline at end of file diff --git a/tenant/api.php b/tenant/api.php new file mode 100644 index 0000000..efd852f --- /dev/null +++ b/tenant/api.php @@ -0,0 +1,225 @@ + false, 'message' => '권한이 없습니다.']); +// exit; +// } + +$pdo = db_connect(); +$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : ''; + +try { + if (!$pdo) throw new Exception("Database connection failed."); + + // DB명이 정의되지 않았을 경우를 대비해 기본값 설정 혹은 mydb.php의 $DB 사용 + // 보통 mydb.php에서 $DB 변수를 제공한다고 가정 + if (!isset($DB)) { + global $DB; + } + + switch ($action) { + case 'get_companies': + // 모든 회사 가져오기 (파트너-자식 구조) + $sql = "SELECT c.*, p.company_name as parent_name, p.barobill_user_id as parent_user_id + FROM {$DB}.barobill_companies c + LEFT JOIN {$DB}.barobill_companies p ON c.parent_id = p.id + ORDER BY c.parent_id ASC, c.id ASC"; + $stmt = $pdo->query($sql); + $companies = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode(['success' => true, 'data' => $companies]); + break; + + case 'save_company': + // 회사 추가/수정 + $id = isset($_POST['id']) ? intval($_POST['id']) : 0; + + $company_name = $_POST['company_name']; + $corp_num = $_POST['corp_num']; + $barobill_user_id = $_POST['barobill_user_id']; + $memo = $_POST['memo']; + + // 1. Find ID of 'cbx0913' (Parent) + $parent_sql = "SELECT id FROM {$DB}.barobill_companies WHERE barobill_user_id = 'cbx0913' LIMIT 1"; + $stmt = $pdo->query($parent_sql); + $parent_row = $stmt->fetch(PDO::FETCH_ASSOC); + + // 만약 'cbx0913' 본인이면 parent_id는 NULL + if ($barobill_user_id === 'cbx0913') { + $parent_id = null; + } else { + // 부모가 있으면 그 ID, 없으면 NULL (혹은 에러처리) + $parent_id = $parent_row ? $parent_row['id'] : null; + } + + if ($id > 0) { + $sql = "UPDATE {$DB}.barobill_companies SET + parent_id = :parent_id, + company_name = :company_name, + corp_num = :corp_num, + barobill_user_id = :barobill_user_id, + memo = :memo + WHERE id = :id"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + } else { + $sql = "INSERT INTO {$DB}.barobill_companies (parent_id, company_name, corp_num, barobill_user_id, memo) + VALUES (:parent_id, :company_name, :corp_num, :barobill_user_id, :memo)"; + $stmt = $pdo->prepare($sql); + } + + $stmt->bindValue(':parent_id', $parent_id, PDO::PARAM_INT); + $stmt->bindValue(':company_name', $company_name); + $stmt->bindValue(':corp_num', $corp_num); + $stmt->bindValue(':barobill_user_id', $barobill_user_id); + $stmt->bindValue(':memo', $memo); + + if (!$stmt->execute()) { + throw new Exception(implode(", ", $stmt->errorInfo())); + } + echo json_encode(['success' => true]); + break; + + case 'delete_company': + $id = intval($_POST['id']); + $sql = "DELETE FROM {$DB}.barobill_companies WHERE id = :id"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + + if (!$stmt->execute()) { + throw new Exception(implode(", ", $stmt->errorInfo())); + } + echo json_encode(['success' => true]); + break; + + case 'get_cards': + $company_id = intval($_GET['company_id']); + $sql = "SELECT * FROM {$DB}.company_cards WHERE company_id = :company_id ORDER BY id DESC"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':company_id', $company_id, PDO::PARAM_INT); + $stmt->execute(); + $cards = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode(['success' => true, 'data' => $cards]); + break; + + case 'save_card': + $id = isset($_POST['id']) ? intval($_POST['id']) : 0; + $company_id = intval($_POST['company_id']); + $card_company_code = $_POST['card_company_code']; + $card_num = $_POST['card_num']; + $web_id = $_POST['web_id']; + $web_pwd = $_POST['web_pwd']; + + if ($id > 0) { + $sql = "UPDATE {$DB}.company_cards SET + card_company_code = :card_company_code, + card_num = :card_num, + web_id = :web_id, + web_pwd = :web_pwd + WHERE id = :id"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + } else { + $sql = "INSERT INTO {$DB}.company_cards (company_id, card_company_code, card_num, web_id, web_pwd) + VALUES (:company_id, :card_company_code, :card_num, :web_id, :web_pwd)"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':company_id', $company_id, PDO::PARAM_INT); + } + + $stmt->bindValue(':card_company_code', $card_company_code); + $stmt->bindValue(':card_num', $card_num); + $stmt->bindValue(':web_id', $web_id); + $stmt->bindValue(':web_pwd', $web_pwd); + + if (!$stmt->execute()) { + throw new Exception(implode(", ", $stmt->errorInfo())); + } + echo json_encode(['success' => true]); + break; + + case 'delete_card': + $id = intval($_POST['id']); + $sql = "DELETE FROM {$DB}.company_cards WHERE id = :id"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + + if (!$stmt->execute()) { + throw new Exception(implode(", ", $stmt->errorInfo())); + } + echo json_encode(['success' => true]); + break; + + case 'get_accounts': + $company_id = intval($_GET['company_id']); + $sql = "SELECT * FROM {$DB}.company_accounts WHERE company_id = :company_id ORDER BY id DESC"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':company_id', $company_id, PDO::PARAM_INT); + $stmt->execute(); + $accounts = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode(['success' => true, 'data' => $accounts]); + break; + + case 'save_account': + $id = isset($_POST['id']) ? intval($_POST['id']) : 0; + $company_id = intval($_POST['company_id']); + $bank_code = $_POST['bank_code']; + $account_num = $_POST['account_num']; + $account_pwd = $_POST['account_pwd']; + + if ($id > 0) { + $sql = "UPDATE {$DB}.company_accounts SET + bank_code = :bank_code, + account_num = :account_num, + account_pwd = :account_pwd + WHERE id = :id"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + } else { + $sql = "INSERT INTO {$DB}.company_accounts (company_id, bank_code, account_num, account_pwd) + VALUES (:company_id, :bank_code, :account_num, :account_pwd)"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':company_id', $company_id, PDO::PARAM_INT); + } + + $stmt->bindValue(':bank_code', $bank_code); + $stmt->bindValue(':account_num', $account_num); + $stmt->bindValue(':account_pwd', $account_pwd); + + if (!$stmt->execute()) { + throw new Exception(implode(", ", $stmt->errorInfo())); + } + echo json_encode(['success' => true]); + break; + + case 'delete_account': + $id = intval($_POST['id']); + $sql = "DELETE FROM {$DB}.company_accounts WHERE id = :id"; + $stmt = $pdo->prepare($sql); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + + if (!$stmt->execute()) { + throw new Exception(implode(", ", $stmt->errorInfo())); + } + echo json_encode(['success' => true]); + break; + + default: + echo json_encode(['success' => false, 'message' => 'Invalid action']); + break; + } +} catch (Exception $e) { + echo json_encode(['success' => false, 'message' => $e->getMessage()]); +} +?> diff --git a/tenant/index.php b/tenant/index.php new file mode 100644 index 0000000..ab2ab93 --- /dev/null +++ b/tenant/index.php @@ -0,0 +1,506 @@ +alert('접근 권한이 없습니다.'); location.href='/';"; +// exit; +// } +?> + + + + + + 바로빌 테넌트 관리 + + + + + + + + + + + + + + + + + + + + +
+ + + +