바로빌 전자세금계산서 솔루션 연결

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
aweso
2026-01-14 09:15:03 +09:00
parent f00e869368
commit 2ab3534278
35 changed files with 3618 additions and 1 deletions

View File

@@ -0,0 +1 @@
2DD6C76C-04DB-44F7-B6E9-3FC0B2211826

View File

@@ -0,0 +1 @@
2DD6C76C-04DB-44F7-B6E9-3FC0B2211826

View File

@@ -0,0 +1,39 @@
===========================================
바로빌 인증서 ID (선택사항)
===========================================
⚠️ 주의: 이것은 로그인 이메일이 아닙니다!
이 파일에는 바로빌 개발자센터에서 등록한
공동인증서 또는 금융인증서의 ID를 입력하세요.
===========================================
설정 방법:
===========================================
1. 바로빌 개발자센터(https://dev.barobill.co.kr) 로그인
- 로그인 이메일: admin@codebridge-x.com (이것 아님!)
2. 개발자센터 메뉴에서 "인증서 관리" 또는 "인증서 등록" 메뉴 찾기
3. 공동인증서 또는 금융인증서 등록
- 인증서 파일(.pfx 또는 .p12) 업로드
- 인증서 비밀번호 입력
4. 등록 후 발급받은 "인증서 ID" 복사
- 예: "CERT-2024-ABC123-XYZ789"
- 또는 숫자/문자 조합의 고유 ID
5. 아래에 인증서 ID 입력:
[여기에 인증서 ID 입력]
===========================================
참고:
===========================================
- 운영 환경: 세금계산서 발행 시 필수입니다
- 테스트 환경: 인증서 ID 없이도 테스트 가능할 수 있습니다
(barobill_test_mode.txt에 "true"가 설정되어 있으면 선택사항)
- 인증서가 없으면 실제 세금계산서를 발행할 수 없지만,
테스트 API에서는 가상의 인증서를 사용할 수 있습니다

View File

@@ -0,0 +1,3 @@
여기에 바로빌 인증서 ID를 입력하세요 (선택사항)
예: cert-id-here

View File

@@ -0,0 +1,37 @@
===========================================
바로빌 CERTKEY (인증서 키) - 필수
===========================================
⚠️ 중요: 이것은 로그인 이메일이 아닙니다!
이 파일에는 바로빌 개발자센터에서 등록한
공동인증서 또는 금융인증서의 CERTKEY를 입력하세요.
===========================================
설정 방법:
===========================================
1. 바로빌 개발자센터(https://dev.barobill.co.kr) 로그인
2. 개발자센터 메뉴에서 "인증서 관리" 또는 "인증서 등록" 메뉴 찾기
3. 공동인증서 또는 금융인증서 등록
- 인증서 파일(.pfx 또는 .p12) 업로드
- 인증서 비밀번호 입력
4. 등록 후 발급받은 "CERTKEY" 복사
- 예: "CERT-2024-ABC123-XYZ789"
- 또는 숫자/문자 조합의 고유 키
5. 아래에 CERTKEY 입력:
[여기에 CERTKEY 입력]
===========================================
참고:
===========================================
- 세금계산서 발행에 필수입니다
- CERTKEY가 없으면 실제 API를 호출할 수 없습니다
- 테스트 환경에서도 CERTKEY가 필요할 수 있습니다

View File

@@ -0,0 +1 @@
6648603713

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1 @@
true

View File

@@ -0,0 +1 @@
cbx0913

1
apikey/claude_api.txt Normal file
View File

@@ -0,0 +1 @@
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

2
apikey/gcs_config.txt Normal file
View File

@@ -0,0 +1,2 @@
bucket_name=codebridge-speech-audio-files

View File

@@ -0,0 +1 @@
AIzaSyAS3bAzmXlhhZHgO3buFiTGzavXZ6ubYq8

1
apikey/google_api.txt Normal file
View File

@@ -0,0 +1 @@
f7d58533aa1dba0db19d799d85f22686684521d2

View File

@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "codebridge-chatbot",
"private_key_id": "bc6e2c8d65ac9567824b1c3bae245f2249c52aed",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDddbnvIMgXEul\nQKi2hY+LS1zJLHDLlVzlhJ3WU8XJP38z2BeINSSwoUXnLLiCrhideDBoY7p6sD//\nFG6/3dNarsj+UwBDUPcbD8aoxjRUM637BNprb0dOexfLifnKxdDYrsGf1ZwaGvk1\nVtyzvZ0FKsmvMmZJhTwPQD1Qng0ewtRs63U2kV/K/2CMB/Vgw2vh2KzbaJtOncBB\nZ++ZnRAlQntnmvQON2hP+Q/N43oHCOXz6TshphN2EoEgncKkuQxkSHFAPBKtAR0B\nf4sfQ6jFBGJaYcLCm1vIioirw6zNam5CvkbJnuhG6DfSiczynOKH71Y5XvldKAn9\nQY9LDQWnAgMBAAECggEAP9hbBUEHV+et8eGn+k6wL6DDQxcYFPfJ61KhN+QPRAhQ\npX+dWCl7vZJAQh3SeyUg9zbOIRsKS48MqGZlMpjjs8hJ8QDtog9tV9KUYoazixmC\nZ+8S5Wro0NLWV+7OwBRTfqO+rVXZ8pEC/BBOcQuroYdzb5232aYCkzy5in7F7FjH\nMCAxUDJhCM4yv05zM/8SV1Ypn6/f1+ZhIpkzCxFlA/Gx85l7pwV6LCaZjYgDiNAY\nDbVEccshrdKXBVKhkyuuGToDLsiRCdish5zdyA8COPr4tflnXvkvQk0MwjZTEygC\nJ8Ceh7oTY44A/rzSqo2kXaCCByGVnFAXJ3z1JoRbCQKBgQDsANCgHZHSzyvgGlyk\nFST7lgS36r1zNEwyGMIZC/edglXqXGeDnelTiSii3MQcq3fqXvXgVr6Pk/vLHf11\nz8zPVg/lJeYhrz6EF2loMN8rhp0yjzYRMT7AZ8kITtCV5HEgmnU+/o4mNwJfN6qK\n26HTSjjmujAjejDL7my+8naSHQKBgQDUBZop2CZdrEALVp3DIw07ZeFEbnG8j5U9\nvJveaz9wZWZnMlxJgc+dk6I1bZHqlIGU3vJ95xJ6JjnoRhPSi1yceaUFEU9UkQLY\n0XuQwsgaFRJLqHcTwZi1Z//IGagtPkAYXDsJDoRr2CEz2lzwd5RV0XCmDoZN9xV6\n+wnOpGlrkwKBgAE3l9vbiy79JorHWAb4nPI3OdsA+O0pLeNsQUQDzckgLPVCeL4z\nCEsIAA+m99P2Bm5NAxOfHuh6qOfJRc9fvPyswvQ5l9BAqR/hRwfkiKIe1Zy3JF4+\nVMaFQoIqdeTwAq1aXpRul6kWy4pWLSj+LP17+oMmHq1wKeRDXIg3k+j5AoGAQGfD\nQNrMLMBaZBdXrSNErbpxB5yVKDZlm29j2diyWK40wTxnFF0+eBuUtq4mGSArjNF8\n0AoVbs2V4Z0IAHkdFNtO6Y8sjf/O4ZYg9wR0TJgCCsGOCo5QmSqSZHKGx9eVGNFL\njaC/URNCYsH+YX2xrbAFjCv1WFGqUMVZYVBIRckCgYEA3yPZlawHMbHtFXFPL+by\nANKqtgX5mKO7E90kqTTumhOpkuywe3okR30wfs/kzX24cL/+brXs7naYyPDYRZz5\nxAMPGa5D4BvVchz1EBQh4lygA6gRlX2Yy/B3grpyZyCaSANX8sReML3wzn+AQze4\nfVaQ2YB6//fwv/5s95n1FAM=\n-----END PRIVATE KEY-----\n",
"client_email": "vertex-ai-client@codebridge-chatbot.iam.gserviceaccount.com",
"client_id": "114574435593304532521",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/vertex-ai-client%40codebridge-chatbot.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@@ -0,0 +1 @@
AIzaSyAS3bAzmXlhhZHgO3buFiTGzavXZ6ubYq8

1
apikey/notion.txt Normal file
View File

@@ -0,0 +1 @@
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

1
apikey/opendart.txt Normal file
View File

@@ -0,0 +1 @@
ad5002bf2c7d2f93eac3c6f9ff1d4b63bf3027bb

View File

@@ -60,7 +60,7 @@
<a href="../barobill/index.php" className="hover:text-blue-600 flex items-center gap-1"> <a href="../barobill/index.php" className="hover:text-blue-600 flex items-center gap-1">
<i data-lucide="layout-dashboard" className="w-4 h-4"></i> 현황 <i data-lucide="layout-dashboard" className="w-4 h-4"></i> 현황
</a> </a>
<a href="../../5130/etax/index.php" className="hover:text-blue-600 flex items-center gap-1"> <a href="../etax/index.php" className="hover:text-blue-600 flex items-center gap-1">
<i data-lucide="file-text" className="w-4 h-4"></i> 세금계산서 <i data-lucide="file-text" className="w-4 h-4"></i> 세금계산서
</a> </a>
<a href="../index.php" className="hover:text-blue-600 flex items-center gap-1"> <a href="../index.php" className="hover:text-blue-600 flex items-center gap-1">

132
etax/README_DB.md Normal file
View File

@@ -0,0 +1,132 @@
# 전자세금계산서 데이터베이스 스키마
## 테이블 구조
### 1. etax_tenants (테넌트 관리)
멀티테넌시 환경에서 각 고객사(테넌트)의 기본 정보를 저장합니다.
**주요 필드:**
- `id`: 테넌트 고유 ID
- `name`: 기업명
- `bizno`: 사업자번호 (UNIQUE)
- `barobill_api_key`: 바로빌 API 키
- `barobill_cert_id`: 바로빌 인증서 ID
- `status`: 활성 상태 (active/inactive/suspended)
### 2. etax_certificates (인증서 관리)
각 테넌트의 공동인증서 또는 금융인증서 정보를 관리합니다.
**주요 필드:**
- `id`: 인증서 고유 ID
- `tenant_id`: 테넌트 ID (FK)
- `type`: 인증서 유형 (public/financial)
- `valid_from`, `valid_to`: 인증서 유효기간
- `barobill_cert_id`: 바로빌 인증서 ID
### 3. etax_tax_invoices (세금계산서 메인)
전자세금계산서의 기본 정보를 저장합니다.
**주요 필드:**
- `id`: 세금계산서 고유 ID
- `tenant_id`: 테넌트 ID (FK)
- `issue_key`: 바로빌 발행 키 (UNIQUE)
- `supply_amt`, `vat`, `total`: 금액 정보
- `write_date`, `supply_date`: 작성일자, 공급일자
- `state`: 상태 (draft/issued/sent/cancelled)
- `nts_receipt_no`: 국세청 접수번호
### 4. etax_parties (공급자/수취자 정보)
세금계산서의 공급자와 수취자 정보를 저장합니다.
**주요 필드:**
- `id`: 고유 ID
- `invoice_id`: 세금계산서 ID (FK)
- `role`: 역할 (supplier/recipient/trustee)
- `bizno`, `corp_name`, `ceo`: 사업자 정보
- `addr`, `email`, `tel`: 연락처 정보
### 5. etax_line_items (품목 상세)
세금계산서의 품목별 상세 정보를 저장합니다.
**주요 필드:**
- `id`: 고유 ID
- `invoice_id`: 세금계산서 ID (FK)
- `item_name`: 품목명
- `qty`, `unit_price`: 수량, 단가
- `supply_amt`, `vat`: 공급가액, 부가세
- `vat_type`: 부가세 유형 (vat/zero/exempt)
### 6. etax_transmission_logs (전송 로그)
바로빌 API 호출 및 국세청 전송 로그를 기록합니다.
**주요 필드:**
- `id`: 로그 고유 ID
- `invoice_id`: 세금계산서 ID (FK)
- `transmission_type`: 전송 유형 (issue/cancel/nts_send)
- `status`: 상태 (success/failed/pending)
- `response_data`: API 응답 데이터
- `error_message`: 에러 메시지
## 설치 방법
### 1. 데이터베이스 생성 (선택사항)
```sql
CREATE DATABASE IF NOT EXISTS `chandj` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `chandj`;
```
### 2. 스키마 실행
```bash
# MySQL/MariaDB 접속
mysql -u chandj -p chandj < etax/db_schema.sql
```
또는 phpMyAdmin이나 다른 DB 관리 도구에서 `etax/db_schema.sql` 파일을 실행하세요.
### 3. 권한 확인
테이블이 정상적으로 생성되었는지 확인:
```sql
SHOW TABLES LIKE 'etax_%';
```
## 사용 방법
### PHP에서 데이터베이스 연결
```php
require_once($_SERVER['DOCUMENT_ROOT'] . "/lib/mydb.php");
$pdo = db_connect();
// 테넌트 조회 예시
$stmt = $pdo->prepare("SELECT * FROM etax_tenants WHERE id = ?");
$stmt->execute([$tenant_id]);
$tenant = $stmt->fetch(PDO::FETCH_ASSOC);
```
### 멀티테넌시 쿼리 예시
```php
// 항상 tenant_id로 필터링
$stmt = $pdo->prepare("
SELECT * FROM etax_tax_invoices
WHERE tenant_id = ? AND state = 'issued'
ORDER BY created_at DESC
");
$stmt->execute([$tenant_id]);
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
```
## 주의사항
1. **외래키 제약조건**: 모든 외래키는 CASCADE 삭제로 설정되어 있어, 부모 레코드 삭제 시 자식 레코드도 자동 삭제됩니다.
2. **인덱스**: 자주 조회되는 필드에 인덱스를 설정했습니다. 추가 인덱스가 필요하면 성능 모니터링 후 추가하세요.
3. **문자셋**: utf8mb4를 사용하여 이모지 등 모든 유니코드 문자를 지원합니다.
4. **보안**:
- `barobill_api_key`는 암호화하여 저장하는 것을 권장합니다.
- `kms_key_ref`는 암호화된 인증서 참조를 저장합니다.
## 마이그레이션
기존 JSON 파일(`invoices_data.json`)에서 데이터를 마이그레이션하려면 별도의 마이그레이션 스크립트를 작성해야 합니다.

73
etax/api/API_URL_GUIDE.md Normal file
View File

@@ -0,0 +1,73 @@
# 바로빌 API 엔드포인트 URL 설정 가이드
## 현재 오류
```
Could not resolve host: test-api.barobill.co.kr
```
이 오류는 DNS 해결 실패로, API 엔드포인트 URL이 잘못되었을 가능성이 높습니다.
## 해결 방법
### 1. 바로빌 개발자센터에서 정확한 URL 확인
1. [바로빌 개발자센터](https://dev.barobill.co.kr) 로그인
2. API 문서 또는 샘플 코드 확인
3. 실제 API 엔드포인트 URL 확인
### 2. 일반적인 바로빌 API URL 패턴
바로빌 API는 다음과 같은 URL 패턴을 사용할 수 있습니다:
#### 운영 환경
- `https://api.barobill.co.kr`
- `https://www.barobill.co.kr/api`
- `https://barobill.co.kr/api`
#### 테스트 환경
- `https://test.barobill.co.kr`
- `https://test-api.barobill.co.kr` (현재 사용 중 - DNS 오류 발생)
- `https://dev.barobill.co.kr/api`
- 개발자센터에서 제공하는 별도 테스트 URL
### 3. 설정 파일 수정
`etax/api/barobill_config.php` 파일의 다음 부분을 수정하세요:
```php
$barobillApiBaseUrl = $isTestMode
? 'https://실제_테스트_URL' // 바로빌 개발자센터에서 확인한 URL
: 'https://실제_운영_URL'; // 바로빌 개발자센터에서 확인한 URL
```
### 4. API 키 확인
현재 `apikey/barobill_api_key.txt` 파일에 API 키가 제대로 입력되어 있는지 확인하세요.
- API 키가 없으면 시뮬레이션 모드로 동작합니다
- API 키가 있으면 실제 바로빌 API를 호출합니다
### 5. 네트워크/DNS 확인
만약 URL이 맞는데도 DNS 오류가 발생한다면:
1. **인터넷 연결 확인**
2. **방화벽 설정 확인**
3. **DNS 서버 변경** (Google DNS: 8.8.8.8, 8.8.4.4)
4. **호스트 파일 확인** (Windows: C:\Windows\System32\drivers\etc\hosts)
### 6. 임시 해결책 (시뮬레이션 모드)
실제 API 연동 전까지는:
1. `apikey/barobill_api_key.txt` 파일을 비워두거나
2. 파일 내용을 주석 처리하면
3. 시뮬레이션 모드로 동작하여 테스트할 수 있습니다
## 참고 자료
- [바로빌 개발자센터](https://dev.barobill.co.kr)
- [바로빌 세금계산서 API 문서](https://dev.barobill.co.kr/docs/references/%EC%84%B8%EA%B8%88%EA%B3%84%EC%82%B0%EC%84%9C-API)
- 바로빌 고객지원: 1544-9256

147
etax/api/README.md Normal file
View File

@@ -0,0 +1,147 @@
# 바로빌 API 연동 가이드
## ⚠️ 중요: 바로빌은 SOAP 웹서비스를 사용합니다
바로빌 API는 REST API가 아닌 **SOAP 웹서비스**를 사용합니다.
- 테스트 환경: `https://testws.baroservice.com/TI.asmx?WSDL`
- 운영 환경: `https://ws.baroservice.com/TI.asmx?WSDL`
## API 키 설정
### 1. 필수 파일 생성
`apikey` 폴더에 다음 파일들을 생성하세요:
#### 필수 파일
- `barobill_cert_key.txt`: CERTKEY (인증서 키)
- 바로빌 개발자센터에서 인증서 등록 후 발급받은 CERTKEY
- 세금계산서 발행에 필수
- `barobill_corp_num.txt`: 사업자번호
- 세금계산서를 발행할 회사의 사업자번호
- 하이픈(-) 없이 숫자만 입력
- 예: `1234567890`
#### 선택 파일
- `barobill_test_mode.txt`: 테스트 모드 사용 시 "test" 또는 "true" 입력
- 테스트 환경: `https://testws.baroservice.com/TI.asmx?WSDL`
- 운영 환경: `https://ws.baroservice.com/TI.asmx?WSDL`
### 2. CERTKEY 발급 방법
1. [바로빌 개발자센터](https://dev.barobill.co.kr) 접속
2. 회원가입 및 로그인
3. 개발자센터 메뉴에서 "인증서 관리" 또는 "인증서 등록" 메뉴 찾기
4. 공동인증서(.pfx 또는 .p12 파일) 또는 금융인증서 업로드
5. 인증서 비밀번호 입력 및 등록
6. 등록 완료 후 발급받은 **CERTKEY** 확인
- 예: "CERT-2024-ABC123-XYZ789" 또는 숫자/문자 조합의 고유 키
7. `apikey/barobill_cert_key.txt` 파일에 CERTKEY 저장
### 3. 사업자번호 설정
1. 세금계산서를 발행할 회사의 사업자번호 확인
2. 하이픈(-) 없이 숫자만 입력
- 예: `123-45-67890``1234567890`
3. `apikey/barobill_corp_num.txt` 파일에 저장
### 4. 테스트 모드 설정
테스트 환경을 사용하려면:
1. `apikey/barobill_test_mode.txt` 파일 생성
2. 파일 내용에 `test` 또는 `true` 입력
3. 저장
## API 엔드포인트
### 세금계산서 발행 (저장 + 발급)
- **SOAP 메서드**: `RegistAndIssueTaxInvoice`
- **파일**: `issue.php`
- **문서**: `etax/docs/barobill-api-doc/TAXINVOICE/RegistAndIssueTaxInvoice.php`
### 세금계산서 조회
- **SOAP 메서드**: `GetTaxInvoice`
- **파일**: `invoices.php`
- **문서**: `etax/docs/barobill-api-doc/TAXINVOICE/GetTaxInvoice.php`
### 세금계산서 상태 조회
- **SOAP 메서드**: `GetTaxInvoiceStateEX`
- **문서**: `etax/docs/barobill-api-doc/TAXINVOICE/GetTaxInvoiceStateEX.php`
### 국세청 전송
- **SOAP 메서드**: `SendToNTS`
- **파일**: `status.php`
- **문서**: `etax/docs/barobill-api-doc/TAXINVOICE/SendToNTS.php`
## SOAP API 사용 예제
### 세금계산서 발행
```php
require_once(__DIR__ . '/barobill_config.php');
$invoiceData = [
'issueKey' => 'MGT20241201123456', // 관리번호 (MgtKey)
'supplierBizno' => '1234567890',
'supplierName' => '공급자 회사명',
'recipientBizno' => '0987654321',
'recipientName' => '수취인 회사명',
'supplyDate' => '2024-12-01',
'items' => [
[
'name' => '품목명',
'spec' => '규격',
'qty' => '1',
'unitPrice' => 10000,
'supplyAmt' => 10000,
'vat' => 1000
]
],
'memo' => '비고'
];
$result = issueTaxInvoice($invoiceData);
```
### 국세청 전송
```php
require_once(__DIR__ . '/barobill_config.php');
$mgtKey = 'MGT20241201123456'; // 관리번호
$result = sendToNTS($mgtKey);
```
## 참고 문서
- [바로빌 개발자센터](https://dev.barobill.co.kr)
- [바로빌 세금계산서 API 문서](https://dev.barobill.co.kr/docs/references/%EC%84%B8%EA%B8%88%EA%B3%84%EC%82%B0%EC%84%9C-API)
- 로컬 문서: `etax/docs/barobill-api-doc/` 폴더
## 주의사항
1. **CERTKEY와 사업자번호는 절대 공개 저장소에 커밋하지 마세요**
2. `.gitignore``apikey/*.txt` (예제 파일 제외) 추가 권장
3. 테스트 환경과 운영 환경의 CERTKEY를 분리하여 관리하세요
4. SOAP 클라이언트는 PHP의 `SoapClient` 클래스를 사용합니다
5. PHP 7.3 이상에서 `SoapClient` 확장이 활성화되어 있어야 합니다
## 문제 해결
### SOAP 클라이언트 생성 실패
- PHP `SoapClient` 확장이 설치되어 있는지 확인
- `php -m | grep soap` 명령으로 확인
- 설치되지 않았다면 PHP 확장 설치 필요
### CERTKEY 오류
- CERTKEY가 올바르게 입력되었는지 확인
- 바로빌 개발자센터에서 인증서가 정상적으로 등록되었는지 확인
- 테스트 모드에서는 CERTKEY가 선택사항일 수 있음
### 사업자번호 오류
- 하이픈(-) 없이 숫자만 입력했는지 확인
- 발행자 사업자번호가 올바른지 확인

View File

@@ -0,0 +1,418 @@
<?php
/**
* 바로빌 API 설정 파일
*
* ⚠️ 중요: 바로빌은 SOAP 웹서비스를 사용합니다 (REST API가 아님)
*
* 사용 방법:
* 1. apikey/barobill_cert_key.txt 파일에 CERTKEY(인증서 키)를 저장하세요
* - 바로빌 개발자센터에서 인증서 등록 후 발급받은 CERTKEY
*
* 2. apikey/barobill_corp_num.txt 파일에 사업자번호를 저장하세요
* - 세금계산서를 발행할 회사의 사업자번호 (하이픈 제외)
*
* 3. 테스트 환경인 경우 apikey/barobill_test_mode.txt 파일에 "test" 또는 "true"를 저장하세요
*/
// load .env file
require_once __DIR__ . '/../../lib/DotEnv.php';
(new DotEnv(__DIR__ . '/../../.env'))->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 읽기 (인증서 키)
// 우선순위: barobill_cert_key.txt > barobill_api_key.txt (기존 호환성)
$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;
}
}
// 기존 barobill_api_key.txt도 CERTKEY로 사용 (호환성)
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 = $content;
// 하이픈 제거
$barobillCorpNum = str_replace('-', '', $barobillCorpNum);
}
}
// 테스트 모드 확인
$isTestMode = false;
if (file_exists($testModeFile)) {
$testMode = trim(file_get_contents($testModeFile));
$isTestMode = (strtolower($testMode) === 'test' || strtolower($testMode) === 'true');
}
// 바로빌 SOAP 웹서비스 URL
// 문서 참고: etax/docs/barobill-api-doc/_lib/BaroService_TI.php
$barobillSoapUrl = $isTestMode
? 'https://testws.baroservice.com/TI.asmx?WSDL' // 테스트 환경
: 'https://ws.baroservice.com/TI.asmx?WSDL'; // 운영 환경
// SOAP 클라이언트 초기화
$barobillSoapClient = null;
// 테스트 모드에서는 CERTKEY 없이도 SOAP 클라이언트 초기화 시도
if (!empty($barobillCertKey) || $isTestMode) {
try {
$barobillSoapClient = new SoapClient($barobillSoapUrl, [
'trace' => true,
'encoding' => 'UTF-8',
'exceptions' => true,
'connection_timeout' => 30
]);
} catch (Throwable $e) {
// SOAP 클라이언트 생성 실패 시 null 유지 (Class not found 등 Fatal Error 포함)
error_log('바로빌 SOAP 클라이언트 생성 실패: ' . $e->getMessage());
}
}
/**
* 바로빌 SOAP 웹서비스 호출 함수
*
* @param string $method SOAP 메서드명 (예: 'RegistAndIssueTaxInvoice')
* @param array $params SOAP 메서드 파라미터
* @return array 응답 데이터
*/
function callBarobillSOAP($method, $params = []) {
global $barobillSoapClient, $barobillCertKey, $barobillCorpNum, $isTestMode;
if (!$barobillSoapClient) {
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/TI.asmx?WSDL' : 'https://ws.baroservice.com/TI.asmx?WSDL'
]
];
}
// 테스트 모드가 아닌 경우 CERTKEY 필수
if (empty($barobillCertKey) && !$isTestMode) {
return [
'success' => false,
'error' => 'CERTKEY가 설정되지 않았습니다. apikey/barobill_cert_key.txt 파일을 확인하세요.'
];
}
// 테스트 모드에서 CERTKEY가 없으면 빈 문자열로 처리 (바로빌 테스트 API가 허용할 수 있음)
if (empty($barobillCertKey) && $isTestMode) {
$barobillCertKey = ''; // 빈 문자열로 시도
}
// 테스트 모드에서 사업자번호가 없으면 더미 사업자번호 사용
if (empty($barobillCorpNum)) {
if ($isTestMode) {
// 테스트 모드: 더미 사업자번호 사용 (바로빌 테스트 API가 허용할 수 있음)
$barobillCorpNum = '1234567890'; // 테스트용 더미 사업자번호
error_log('바로빌 테스트 모드: 사업자번호가 없어서 더미 사업자번호를 사용합니다.');
} else {
// 운영 모드: 사업자번호 필수
return [
'success' => false,
'error' => '사업자번호가 설정되지 않았습니다. apikey/barobill_corp_num.txt 파일을 확인하세요.'
];
}
}
try {
// CERTKEY와 CorpNum을 파라미터에 자동 추가
if (!isset($params['CERTKEY'])) {
$params['CERTKEY'] = $barobillCertKey;
}
if (!isset($params['CorpNum']) && !isset($params['CorpNum'])) {
// CorpNum이 파라미터에 없으면 추가 (일부 메서드는 Invoice 내부에 있음)
if (!isset($params['Invoice']['InvoicerParty']['CorpNum'])) {
// Invoice 구조가 없으면 최상위에 추가
if (!isset($params['CorpNum'])) {
$params['CorpNum'] = $barobillCorpNum;
}
}
}
// 디버깅: 전달되는 파라미터 로그 (민감 정보는 마스킹)
error_log('바로빌 API 호출 - Method: ' . $method . ', CorpNum: ' . $barobillCorpNum . ', CERTKEY: ' . (empty($barobillCertKey) ? '(없음)' : substr($barobillCertKey, 0, 10) . '...'));
// SOAP 메서드 호출
$result = $barobillSoapClient->$method($params);
// 결과에서 Result 속성 추출
$resultProperty = $method . 'Result';
if (isset($result->$resultProperty)) {
$resultData = $result->$resultProperty;
// 결과가 음수면 오류 코드
if (is_numeric($resultData) && $resultData < 0) {
// 오류 코드에 따른 메시지 매핑
$errorMessages = [
-11101 => '사업자번호가 설정되지 않았거나 유효하지 않습니다. apikey/barobill_corp_num.txt 파일에 올바른 사업자번호를 입력하세요.',
-11102 => 'CERTKEY가 유효하지 않습니다. 바로빌 개발자센터에서 발급받은 CERTKEY를 확인하세요.',
-11103 => '인증서가 만료되었거나 유효하지 않습니다.',
-26001 => '발행에 필요한 공동인증서가 등록되어 있지 않습니다. 바로빌 웹사이트(https://www.barobill.co.kr)에 로그인하여 공동인증서를 등록하고, CERTKEY와 연결되어 있는지 확인하세요.',
-32000 => '알 수 없는 오류가 발생했습니다.',
];
$errorMessage = isset($errorMessages[$resultData])
? $errorMessages[$resultData]
: '바로빌 API 오류 코드: ' . $resultData;
// GetErrString API로 상세 오류 메시지 조회 시도 (CERTKEY가 있는 경우)
$detailedError = null;
if (!empty($barobillCertKey) && $barobillSoapClient) {
try {
$errStringResult = $barobillSoapClient->GetErrString([
'CERTKEY' => $barobillCertKey,
'ErrCode' => $resultData
]);
if (isset($errStringResult->GetErrStringResult) && $errStringResult->GetErrStringResult >= 0) {
$detailedError = $errStringResult->GetErrStringResult;
}
} catch (Exception $e) {
// GetErrString 호출 실패 시 무시
}
}
return [
'success' => false,
'error' => $errorMessage,
'error_code' => $resultData,
'error_detail' => $detailedError ? "상세 오류: " . $detailedError : null,
'soap_request' => $barobillSoapClient->__getLastRequest(),
'soap_response' => $barobillSoapClient->__getLastResponse()
];
}
return [
'success' => true,
'data' => $resultData,
'soap_request' => $barobillSoapClient->__getLastRequest(),
'soap_response' => $barobillSoapClient->__getLastResponse()
];
}
return [
'success' => true,
'data' => $result,
'soap_request' => $barobillSoapClient->__getLastRequest(),
'soap_response' => $barobillSoapClient->__getLastResponse()
];
} catch (SoapFault $e) {
return [
'success' => false,
'error' => 'SOAP 오류: ' . $e->getMessage(),
'error_code' => $e->getCode(),
'error_detail' => [
'fault_code' => $e->faultcode ?? null,
'fault_string' => $e->faultstring ?? null,
'soap_request' => $barobillSoapClient ? $barobillSoapClient->__getLastRequest() : null,
'soap_response' => $barobillSoapClient ? $barobillSoapClient->__getLastResponse() : null
]
];
} catch (Throwable $e) {
return [
'success' => false,
'error' => 'API 호출 오류 (치명적): ' . $e->getMessage(),
'error_detail' => [
'exception_type' => get_class($e),
'soap_request' => $barobillSoapClient ? $barobillSoapClient->__getLastRequest() : null,
'soap_response' => $barobillSoapClient ? $barobillSoapClient->__getLastResponse() : null
]
];
}
}
/**
* 바로빌 세금계산서 발행 (저장 + 발급)
*
* 문서 참고: etax/docs/barobill-api-doc/TAXINVOICE/RegistAndIssueTaxInvoice.php
*
* @param array $invoiceData 세금계산서 데이터
* @return array 응답 데이터
*/
function issueTaxInvoice($invoiceData) {
global $barobillCorpNum;
// MgtKey 생성 (관리번호) - 유니크한 키 생성
$mgtKey = $invoiceData['issueKey'] ?? 'MGT' . date('YmdHis') . rand(1000, 9999);
// 공급가액, 부가세, 합계 계산
$supplyAmt = 0;
$vat = 0;
foreach ($invoiceData['items'] ?? [] as $item) {
$itemSupplyAmt = floatval($item['supplyAmt'] ?? 0);
$itemVat = floatval($item['vat'] ?? 0);
$supplyAmt += $itemSupplyAmt;
$vat += $itemVat;
}
$total = $supplyAmt + $vat;
// TaxType 결정: 부가세가 0원이면 영세(2) 또는 면세(3)로 설정
// 과세(1)는 부가세가 0원 이상이어야 함
$taxType = 1; // 기본값: 과세
if ($vat == 0) {
// 부가세가 0원이면 영세로 설정 (또는 면세로 설정 가능)
$taxType = 2; // 2: 영세
}
// 바로빌 SOAP API 스펙에 맞게 데이터 변환
// 문서 참고: etax/docs/barobill-api-doc/TAXINVOICE/RegistAndIssueTaxInvoice.php
$taxInvoice = [
'IssueDirection' => 1, // 1: 정발행, 2: 역발행
'TaxInvoiceType' => 1, // 1: 세금계산서, 2: 계산서
'ModifyCode' => '', // 수정사유코드 (신규발행시 빈값)
'TaxType' => $taxType, // 1: 과세, 2: 영세, 3: 면세 (부가세가 0이면 영세로 설정)
'TaxCalcType' => 1, // 1: 소계합계, 2: 항목합계
'PurposeType' => 2, // 1: 영수, 2: 청구, 3: 없음
'WriteDate' => date('Ymd', strtotime($invoiceData['writeDate'] ?? date('Y-m-d'))), // 작성일자 (YYYYMMDD)
'AmountTotal' => number_format($supplyAmt, 0, '', ''), // 공급가액 합계
'TaxTotal' => number_format($vat, 0, '', ''), // 부가세 합계
'TotalAmount' => number_format($total, 0, '', ''), // 합계금액
'Cash' => '0', // 현금
'ChkBill' => '0', // 어음
'Note' => '0', // 외상
'Credit' => number_format($total, 0, '', ''), // 외상미수금 (합계금액과 일치해야 함)
'Remark1' => $invoiceData['memo'] ?? '', // 비고1
'Remark2' => '', // 비고2
'Remark3' => '', // 비고3
'Kwon' => '', // 권
'Ho' => '', // 호
'SerialNum' => '', // 일련번호
'InvoicerParty' => [
'MgtNum' => $mgtKey, // 관리번호
'CorpNum' => $barobillCorpNum, // 발행자 사업자번호 (CERTKEY와 연결된 사업자번호 사용)
'TaxRegID' => '', // 종사업장번호
'CorpName' => $invoiceData['supplierName'] ?? '', // 상호
'CEOName' => $invoiceData['supplierCeo'] ?? '', // 대표자명
'Addr' => $invoiceData['supplierAddr'] ?? '', // 주소
'BizType' => '', // 업태
'BizClass' => '', // 종목
'ContactID' => $invoiceData['supplierContactId'] ?? 'cbx0913', // 담당자 아이디 (바로빌 웹페이지 ID)
'ContactName' => $invoiceData['supplierContact'] ?? '', // 담당자명
'TEL' => $invoiceData['supplierTel'] ?? '', // 전화번호
'HP' => '', // 휴대폰
'Email' => $invoiceData['supplierEmail'] ?? '', // 이메일
],
'InvoiceeParty' => [
'MgtNum' => '', // 관리번호
'CorpNum' => str_replace('-', '', $invoiceData['recipientBizno'] ?? ''), // 사업자번호
'TaxRegID' => '', // 종사업장번호
'CorpName' => $invoiceData['recipientName'] ?? '', // 상호
'CEOName' => $invoiceData['recipientCeo'] ?? '', // 대표자명
'Addr' => $invoiceData['recipientAddr'] ?? '', // 주소
'BizType' => '', // 업태
'BizClass' => '', // 종목
'ContactID' => '', // 담당자 아이디
'ContactName' => $invoiceData['recipientContact'] ?? '', // 담당자명
'TEL' => $invoiceData['recipientTel'] ?? '', // 전화번호
'HP' => '', // 휴대폰
'Email' => $invoiceData['recipientEmail'] ?? '', // 이메일
],
'BrokerParty' => [], // 위수탁 거래시에만 사용
'TaxInvoiceTradeLineItems' => [
'TaxInvoiceTradeLineItem' => []
]
];
// 품목 데이터 변환
foreach ($invoiceData['items'] ?? [] as $item) {
$taxInvoice['TaxInvoiceTradeLineItems']['TaxInvoiceTradeLineItem'][] = [
'PurchaseExpiry' => '', // 공제기한
'Name' => $item['name'] ?? '', // 품명
'Information' => $item['spec'] ?? '', // 규격
'ChargeableUnit' => $item['qty'] ?? '1', // 수량
'UnitPrice' => number_format(floatval($item['unitPrice'] ?? 0), 0, '', ''), // 단가
'Amount' => number_format(floatval($item['supplyAmt'] ?? 0), 0, '', ''), // 공급가액
'Tax' => number_format(floatval($item['vat'] ?? 0), 0, '', ''), // 부가세
'Description' => $item['description'] ?? '', // 비고
];
}
// SOAP 메서드 호출
$params = [
'CorpNum' => $barobillCorpNum, // 발행자 사업자번호
'Invoice' => $taxInvoice,
'SendSMS' => false, // SMS 발송 여부
'ForceIssue' => false, // 강제발행 여부
'MailTitle' => '', // 이메일 제목
];
return callBarobillSOAP('RegistAndIssueTaxInvoice', $params);
}
/**
* 바로빌 세금계산서 조회
*
* 문서 참고: etax/docs/barobill-api-doc/TAXINVOICE/GetTaxInvoice.php
*
* @param string $mgtKey 관리번호 (MgtKey)
* @return array 응답 데이터
*/
function getTaxInvoice($mgtKey) {
global $barobillCorpNum;
$params = [
'CorpNum' => $barobillCorpNum,
'MgtKey' => $mgtKey
];
return callBarobillSOAP('GetTaxInvoice', $params);
}
/**
* 바로빌 세금계산서 상태 조회
*
* 문서 참고: etax/docs/barobill-api-doc/TAXINVOICE/GetTaxInvoiceStateEX.php
*
* @param string $mgtKey 관리번호 (MgtKey)
* @return array 응답 데이터
*/
function getTaxInvoiceState($mgtKey) {
global $barobillCorpNum;
$params = [
'CorpNum' => $barobillCorpNum,
'MgtKey' => $mgtKey
];
return callBarobillSOAP('GetTaxInvoiceStateEX', $params);
}
/**
* 바로빌 세금계산서 국세청 전송
*
* 문서 참고: etax/docs/barobill-api-doc/TAXINVOICE/SendToNTS.php
*
* @param string $mgtKey 관리번호 (MgtKey)
* @return array 응답 데이터
*/
function sendToNTS($mgtKey) {
global $barobillCorpNum;
$params = [
'CorpNum' => $barobillCorpNum,
'MgtKey' => $mgtKey
];
return callBarobillSOAP('SendToNTS', $params);
}
?>

34
etax/api/debug_test.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
header('Content-Type: text/plain');
echo "Current Dir: " . __DIR__ . "\n";
echo "DotEnv Path: " . __DIR__ . '/../../lib/DotEnv.php' . "\n";
echo "Env File Path: " . __DIR__ . '/../../.env' . "\n";
if (file_exists(__DIR__ . '/../../lib/DotEnv.php')) {
echo "DotEnv file exists.\n";
require_once __DIR__ . '/../../lib/DotEnv.php';
echo "DotEnv loaded.\n";
} else {
echo "DotEnv file NOT found.\n";
}
if (file_exists(__DIR__ . '/../../.env')) {
echo ".env file exists.\n";
try {
(new DotEnv(__DIR__ . '/../../.env'))->load();
echo ".env loaded.\n";
} catch (Exception $e) {
echo "Error loading .env: " . $e->getMessage() . "\n";
}
} else {
echo ".env file NOT found.\n";
}
$root = getenv('DOCUMENT_ROOT');
echo "DOCUMENT_ROOT from getenv: " . var_export($root, true) . "\n";
echo "DOCUMENT_ROOT from \$_ENV: " . var_export($_ENV['DOCUMENT_ROOT'] ?? 'unset', true) . "\n";
echo "DOCUMENT_ROOT from \$_SERVER: " . var_export($_SERVER['DOCUMENT_ROOT'] ?? 'unset', true) . "\n";

163
etax/api/delete.php Normal file
View File

@@ -0,0 +1,163 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
$method = $_SERVER['REQUEST_METHOD'];
if ($method !== 'POST' && $method !== 'DELETE') {
http_response_code(405);
echo json_encode([
"success" => false,
"error" => "Method not allowed"
], JSON_UNESCAPED_UNICODE);
exit;
}
// 요청 본문에서 invoiceId 가져오기
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$invoiceId = $input['invoiceId'] ?? $_GET['invoiceId'] ?? null;
// 디버깅: 요청 데이터 로그
error_log("Delete request - Raw input: " . $rawInput);
error_log("Delete request - Parsed input: " . print_r($input, true));
error_log("Delete request - invoiceId: " . var_export($invoiceId, true));
if (!$invoiceId) {
http_response_code(400);
echo json_encode([
"success" => false,
"error" => "invoiceId is required",
"debug" => [
"rawInput" => $rawInput,
"parsedInput" => $input,
"getParams" => $_GET
]
], JSON_UNESCAPED_UNICODE);
exit;
}
$dataFile = __DIR__ . '/invoices_data.json';
// 파일에서 데이터 읽기
$existingData = [];
if (file_exists($dataFile)) {
$fileContent = file_get_contents($dataFile);
if ($fileContent !== false) {
$existingData = json_decode($fileContent, true);
if (!is_array($existingData)) {
$existingData = [];
}
} else {
error_log("Failed to read invoices_data.json file");
}
} else {
error_log("invoices_data.json file does not exist");
}
if (!isset($existingData['invoices'])) {
$existingData['invoices'] = [];
}
// 디버깅: 현재 파일에 있는 ID 목록
$currentIds = array_map(function($inv) {
return $inv['id'] ?? 'no-id';
}, $existingData['invoices']);
error_log("Current invoice IDs in file: " . implode(', ', $currentIds));
error_log("Looking for invoiceId: " . $invoiceId);
// 삭제된 ID 목록 파일
$deletedIdsFile = __DIR__ . '/deleted_ids.json';
$deletedIds = [];
// 삭제된 ID 목록 읽기
if (file_exists($deletedIdsFile)) {
$deletedContent = file_get_contents($deletedIdsFile);
if ($deletedContent !== false) {
$deletedIds = json_decode($deletedContent, true);
if (!is_array($deletedIds)) {
$deletedIds = [];
}
}
}
// 기본 mock 데이터인 경우, 삭제된 ID 목록에 추가
$defaultMockIds = ['inv_001', 'inv_002', 'inv_003'];
if (in_array($invoiceId, $defaultMockIds)) {
// 이미 삭제된 경우
if (in_array($invoiceId, $deletedIds)) {
http_response_code(404);
echo json_encode([
"success" => false,
"error" => "Invoice not found",
"message" => "이미 삭제된 항목입니다."
], JSON_UNESCAPED_UNICODE);
exit;
}
// 삭제된 ID 목록에 추가
$deletedIds[] = $invoiceId;
$deletedIds = array_unique($deletedIds); // 중복 제거
file_put_contents($deletedIdsFile, json_encode($deletedIds, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode([
"success" => true,
"message" => "Invoice deleted successfully",
"deletedId" => $invoiceId
], JSON_UNESCAPED_UNICODE);
exit;
}
// 해당 ID의 인보이스 찾아서 삭제
$beforeCount = count($existingData['invoices']);
$foundInvoice = null;
foreach ($existingData['invoices'] as $index => $invoice) {
$currentId = $invoice['id'] ?? '';
// 문자열 비교 (타입 변환)
if ((string)$currentId === (string)$invoiceId) {
$foundInvoice = $invoice;
unset($existingData['invoices'][$index]);
break;
}
}
$existingData['invoices'] = array_values($existingData['invoices']); // 인덱스 재정렬
$afterCount = count($existingData['invoices']);
if ($beforeCount === $afterCount) {
// 삭제할 항목이 없었음
error_log("Invoice not found - beforeCount: $beforeCount, afterCount: $afterCount");
http_response_code(404);
echo json_encode([
"success" => false,
"error" => "Invoice not found",
"message" => "해당 세금계산서를 찾을 수 없습니다. 이미 삭제되었거나 존재하지 않는 항목일 수 있습니다.",
"debug" => [
"invoiceId" => $invoiceId,
"invoiceIdType" => gettype($invoiceId),
"totalInvoices" => $beforeCount,
"availableIds" => $currentIds
]
], JSON_UNESCAPED_UNICODE);
exit;
}
// 파일에 저장
$jsonData = json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
$saveResult = @file_put_contents($dataFile, $jsonData);
if ($saveResult === false) {
http_response_code(500);
echo json_encode([
"success" => false,
"error" => "Failed to save data"
], JSON_UNESCAPED_UNICODE);
exit;
}
echo json_encode([
"success" => true,
"message" => "Invoice deleted successfully",
"deletedId" => $invoiceId
], JSON_UNESCAPED_UNICODE);
?>

View File

@@ -0,0 +1,5 @@
[
"inv_001",
"inv_002",
"inv_003"
]

170
etax/api/invoices.php Normal file
View File

@@ -0,0 +1,170 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
// Mock Data - 가상의 전자세금계산서 발행 내역
$invoices = [
[
"id" => "inv_001",
"issueKey" => "BARO-2024-001",
"supplierBizno" => "123-45-67890",
"supplierName" => "(주)건축자재",
"recipientBizno" => "987-65-43210",
"recipientName" => "대박 건설(주)",
"supplyDate" => "2024-11-01",
"items" => [
[
"name" => "시멘트 50kg",
"qty" => 100,
"unitPrice" => 8000,
"supplyAmt" => 800000,
"vat" => 80000,
"vatType" => "vat"
],
[
"name" => "철근 10mm",
"qty" => 50,
"unitPrice" => 12000,
"supplyAmt" => 600000,
"vat" => 60000,
"vatType" => "vat"
]
],
"totalSupplyAmt" => 1400000,
"totalVat" => 140000,
"total" => 1540000,
"status" => "sent",
"ntsReceiptNo" => "NTS-2024-001234",
"memo" => "정기 납품",
"createdAt" => "2024-11-01T10:30:00",
"sentAt" => "2024-11-01T10:35:00"
],
[
"id" => "inv_002",
"issueKey" => "BARO-2024-002",
"supplierBizno" => "123-45-67890",
"supplierName" => "(주)건축자재",
"recipientBizno" => "111-22-33333",
"recipientName" => "강남 부동산(주)",
"supplyDate" => "2024-11-05",
"items" => [
[
"name" => "도배지",
"qty" => 200,
"unitPrice" => 5000,
"supplyAmt" => 1000000,
"vat" => 100000,
"vatType" => "vat"
]
],
"totalSupplyAmt" => 1000000,
"totalVat" => 100000,
"total" => 1100000,
"status" => "issued",
"memo" => "",
"createdAt" => "2024-11-05T14:20:00"
],
[
"id" => "inv_003",
"issueKey" => "BARO-2024-003",
"supplierBizno" => "123-45-67890",
"supplierName" => "(주)건축자재",
"recipientBizno" => "555-66-77777",
"recipientName" => "성수 인테리어(주)",
"supplyDate" => "2024-11-10",
"items" => [
[
"name" => "타일 30x30",
"qty" => 500,
"unitPrice" => 3000,
"supplyAmt" => 1500000,
"vat" => 150000,
"vatType" => "vat"
],
[
"name" => "접착제",
"qty" => 20,
"unitPrice" => 15000,
"supplyAmt" => 300000,
"vat" => 30000,
"vatType" => "vat"
]
],
"totalSupplyAmt" => 1800000,
"totalVat" => 180000,
"total" => 1980000,
"status" => "sent",
"ntsReceiptNo" => "NTS-2024-001567",
"memo" => "긴급 납품",
"createdAt" => "2024-11-10T09:15:00",
"sentAt" => "2024-11-10T09:20:00"
]
];
// 삭제된 ID 목록 읽기
$deletedIdsFile = __DIR__ . '/deleted_ids.json';
$deletedIds = [];
if (file_exists($deletedIdsFile)) {
$deletedContent = file_get_contents($deletedIdsFile);
if ($deletedContent !== false) {
$deletedIds = json_decode($deletedContent, true);
if (!is_array($deletedIds)) {
$deletedIds = [];
}
}
}
// 기본 mock 데이터에서 삭제된 항목 제외
$invoices = array_filter($invoices, function($invoice) use ($deletedIds) {
return !in_array($invoice['id'] ?? '', $deletedIds);
});
$invoices = array_values($invoices); // 인덱스 재정렬
// 파일에서 저장된 데이터 읽기 (있는 경우)
$dataFile = __DIR__ . '/invoices_data.json';
if (file_exists($dataFile)) {
$fileContent = file_get_contents($dataFile);
if ($fileContent !== false) {
$savedData = json_decode($fileContent, true);
if (json_last_error() === JSON_ERROR_NONE && isset($savedData['invoices']) && is_array($savedData['invoices']) && count($savedData['invoices']) > 0) {
// 저장된 데이터에서도 삭제된 항목 제외
$savedData['invoices'] = array_filter($savedData['invoices'], function($invoice) use ($deletedIds) {
return !in_array($invoice['id'] ?? '', $deletedIds);
});
$savedData['invoices'] = array_values($savedData['invoices']);
// 저장된 데이터를 우선하고, 기본 mock 데이터와 병합
// 저장된 데이터가 최신이므로 먼저 배치
$invoices = array_merge($savedData['invoices'], $invoices);
// 중복 제거 (id 기준) - 먼저 나온 것이 유지됨 (저장된 데이터 우선)
$uniqueInvoices = [];
$seenIds = [];
foreach ($invoices as $invoice) {
if (!in_array($invoice['id'], $seenIds)) {
$uniqueInvoices[] = $invoice;
$seenIds[] = $invoice['id'];
}
}
$invoices = $uniqueInvoices;
} elseif (json_last_error() !== JSON_ERROR_NONE) {
error_log("invoices_data.json JSON 파싱 오류: " . json_last_error_msg());
}
} else {
error_log("invoices_data.json 파일 읽기 실패: " . $dataFile);
}
}
// 최신순 정렬
usort($invoices, function($a, $b) {
return strtotime($b['createdAt'] ?? $b['supplyDate']) - strtotime($a['createdAt'] ?? $a['supplyDate']);
});
$response = [
"success" => true,
"invoices" => $invoices,
"count" => count($invoices)
];
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
?>

318
etax/api/invoices_data.json Normal file
View File

@@ -0,0 +1,318 @@
{
"invoices": [
{
"id": "inv_1764734934",
"issueKey": "MGT202512031308547398",
"mgtKey": "MGT202512031308547398",
"supplierBizno": "664-86-03713",
"supplierName": "(주)코드브릿지엑스",
"recipientBizno": "843-22-01859",
"recipientName": "조은지게차",
"supplyDate": "2025-11-06",
"items": [
{
"name": "시멘트 50kg",
"qty": 49,
"unitPrice": 37827,
"vatType": "zero",
"supplyAmt": 1853523,
"vat": 0,
"total": 1853523
},
{
"name": "접착제",
"qty": 32,
"unitPrice": 377796,
"vatType": "exempt",
"supplyAmt": 12089472,
"vat": 0,
"total": 12089472
}
],
"totalSupplyAmt": 13942995,
"totalVat": 0,
"total": 13942995,
"status": "issued",
"memo": "긴급 납품",
"createdAt": "2025-12-03T13:08:54",
"barobillInvoiceId": "1"
},
{
"id": "inv_1764735037",
"issueKey": "MGT202512031310379183",
"mgtKey": "MGT202512031310379183",
"supplierBizno": "664-86-03713",
"supplierName": "(주)코드브릿지엑스",
"recipientBizno": "311-46-00378",
"recipientName": "김인태",
"supplyDate": "2025-11-23",
"items": [
{
"name": "타일 30x30",
"qty": 19,
"unitPrice": 152719,
"vatType": "exempt",
"supplyAmt": 2901661,
"vat": 0,
"total": 2901661
},
{
"name": "욕조",
"qty": 48,
"unitPrice": 202442,
"vatType": "exempt",
"supplyAmt": 9717216,
"vat": 0,
"total": 9717216
}
],
"totalSupplyAmt": 12618877,
"totalVat": 0,
"total": 12618877,
"status": "issued",
"memo": "샘플 납품",
"createdAt": "2025-12-03T13:10:37",
"barobillInvoiceId": "1"
},
{
"id": "inv_1765062685",
"issueKey": "MGT202512070811254622",
"mgtKey": "MGT202512070811254622",
"supplierBizno": "664-86-03713",
"supplierName": "(주)코드브릿지엑스",
"recipientBizno": "406-05-25709",
"recipientName": "스카이익스프레스",
"supplyDate": "2025-11-09",
"items": [
{
"name": "배관자재",
"qty": 82,
"unitPrice": 283655,
"vatType": "vat",
"supplyAmt": 23259710,
"vat": 2325971,
"total": 25585681
},
{
"name": "목재 합판",
"qty": 97,
"unitPrice": 196766,
"vatType": "vat",
"supplyAmt": 19086302,
"vat": 1908630,
"total": 20994932
},
{
"name": "유리 5mm",
"qty": 51,
"unitPrice": 240496,
"vatType": "vat",
"supplyAmt": 12265296,
"vat": 1226529,
"total": 13491825
}
],
"totalSupplyAmt": 54611308,
"totalVat": 5461130,
"total": 60072438,
"status": "issued",
"memo": "계약 납품",
"createdAt": "2025-12-07T08:11:25",
"barobillInvoiceId": "1"
},
{
"id": "inv_1765114676",
"issueKey": "MGT202512072237565827",
"mgtKey": "MGT202512072237565827",
"supplierBizno": "664-86-03713",
"supplierName": "(주)코드브릿지엑스",
"recipientBizno": "107-81-78114",
"recipientName": "(주)이상네트웍스",
"supplyDate": "2025-11-14",
"items": [
{
"name": "배관자재",
"qty": 6,
"unitPrice": 238878,
"vatType": "vat",
"supplyAmt": 1433268,
"vat": 143326,
"total": 1576594
},
{
"name": "접착제",
"qty": 79,
"unitPrice": 313735,
"vatType": "vat",
"supplyAmt": 24785065,
"vat": 2478506,
"total": 27263571
},
{
"name": "변기",
"qty": 80,
"unitPrice": 441883,
"vatType": "vat",
"supplyAmt": 35350640,
"vat": 3535064,
"total": 38885704
}
],
"totalSupplyAmt": 61568973,
"totalVat": 6156896,
"total": 67725869,
"status": "issued",
"memo": "보수 납품",
"createdAt": "2025-12-07T22:37:56",
"barobillInvoiceId": "1"
},
{
"id": "inv_1765114749",
"issueKey": "MGT202512072239096510",
"mgtKey": "MGT202512072239096510",
"supplierBizno": "664-86-03713",
"supplierName": "(주)코드브릿지엑스",
"recipientBizno": "843-22-01859",
"recipientName": "조은지게차",
"supplyDate": "2025-11-24",
"items": [
{
"name": "유리 5mm",
"qty": 91,
"unitPrice": 409294,
"vatType": "vat",
"supplyAmt": 37245754,
"vat": 3724575,
"total": 40970329
},
{
"name": "접착제",
"qty": 39,
"unitPrice": 320606,
"vatType": "vat",
"supplyAmt": 12503634,
"vat": 1250363,
"total": 13753997
},
{
"name": "변기",
"qty": 33,
"unitPrice": 140978,
"vatType": "vat",
"supplyAmt": 4652274,
"vat": 465227,
"total": 5117501
}
],
"totalSupplyAmt": 54401662,
"totalVat": 5440165,
"total": 59841827,
"status": "issued",
"memo": "정기 납품",
"createdAt": "2025-12-07T22:39:09",
"barobillInvoiceId": "1"
},
{
"id": "inv_1765120032",
"issueKey": "MGT202512080007129289",
"mgtKey": "MGT202512080007129289",
"supplierBizno": "664-86-03713",
"supplierName": "(주)코드브릿지엑스",
"recipientBizno": "107-81-78114",
"recipientName": "(주)이상네트웍스",
"supplyDate": "2025-11-13",
"items": [
{
"name": "유리 5mm",
"qty": 90,
"unitPrice": 122746,
"vatType": "vat",
"supplyAmt": 11047140,
"vat": 1104714,
"total": 12151854
}
],
"totalSupplyAmt": 11047140,
"totalVat": 1104714,
"total": 12151854,
"status": "issued",
"memo": "보수 납품",
"createdAt": "2025-12-08T00:07:12",
"barobillInvoiceId": "1"
},
{
"id": "inv_1768349618",
"issueKey": "MGT202601140913384780",
"mgtKey": "MGT202601140913384780",
"supplierBizno": "664-86-03713",
"supplierName": "(주)코드브릿지엑스",
"recipientBizno": "107-81-78114",
"recipientName": "(주)이상네트웍스",
"supplyDate": "2026-01-10",
"items": [
{
"name": "페인트 18L",
"qty": 79,
"unitPrice": 90747,
"vatType": "vat",
"supplyAmt": 7169013,
"vat": 716901,
"total": 7885914
},
{
"name": "접착제",
"qty": 67,
"unitPrice": 162463,
"vatType": "vat",
"supplyAmt": 10885021,
"vat": 1088502,
"total": 11973523
},
{
"name": "조명기구",
"qty": 25,
"unitPrice": 408336,
"vatType": "vat",
"supplyAmt": 10208400,
"vat": 1020840,
"total": 11229240
}
],
"totalSupplyAmt": 28262434,
"totalVat": 2826243,
"total": 31088677,
"status": "issued",
"memo": "교체 납품",
"createdAt": "2026-01-14T09:13:38",
"barobillInvoiceId": "1"
},
{
"id": "inv_1768349638",
"issueKey": "MGT202601140913583219",
"mgtKey": "MGT202601140913583219",
"supplierBizno": "664-86-03713",
"supplierName": "(주)코드브릿지엑스",
"recipientBizno": "311-46-00378",
"recipientName": "김인태",
"supplyDate": "2026-01-14",
"items": [
{
"name": "변기",
"qty": 68,
"unitPrice": 136303,
"vatType": "vat",
"supplyAmt": 9268604,
"vat": 926860,
"total": 10195464
}
],
"totalSupplyAmt": 9268604,
"totalVat": 926860,
"total": 10195464,
"status": "issued",
"memo": "A\/S 납품",
"createdAt": "2026-01-14T09:13:58",
"barobillInvoiceId": "1"
}
]
}

226
etax/api/issue.php Normal file
View File

@@ -0,0 +1,226 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
// 바로빌 API 설정 로드
require_once(__DIR__ . '/barobill_config.php');
// POST 데이터 읽기
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
http_response_code(400);
echo json_encode([
"success" => false,
"error" => "Invalid request data"
], JSON_UNESCAPED_UNICODE);
exit;
}
// 바로빌 API 호출 또는 시뮬레이션
// 테스트 모드: SOAP 클라이언트만 있으면 실제 API 호출 시도 (CERTKEY는 선택사항)
// 운영 모드: SOAP 클라이언트와 CERTKEY 모두 필요
$useRealAPI = !empty($barobillSoapClient) && ($isTestMode || !empty($barobillCertKey));
// 디버깅 정보: 실제 API 호출 여부 확인
$debugInfo = [
'hasSoapClient' => !empty($barobillSoapClient),
'hasCertKey' => !empty($barobillCertKey),
'hasCorpNum' => !empty($barobillCorpNum),
'isTestMode' => $isTestMode ?? false,
'soapUrl' => $barobillSoapUrl ?? null,
'willUseRealAPI' => $useRealAPI
];
if ($useRealAPI) {
// 실제 바로빌 SOAP API 호출
$apiResult = issueTaxInvoice($input);
if ($apiResult['success']) {
$apiData = $apiResult['data'];
// SOAP 응답에서 MgtKey 추출 (RegistAndIssueTaxInvoice는 MgtKey를 반환)
// 응답이 숫자(양수)면 성공, 그 값이 바로빌 세금계산서 ID일 수 있음
// 또는 객체일 경우 MgtKey 속성 확인
$mgtKey = '';
if (is_object($apiData) && isset($apiData->MgtKey)) {
$mgtKey = $apiData->MgtKey;
} elseif (is_array($apiData) && isset($apiData['MgtKey'])) {
$mgtKey = $apiData['MgtKey'];
} else {
// MgtKey가 없으면 입력에서 생성한 키 사용
$mgtKey = $input['issueKey'] ?? 'MGT' . date('YmdHis') . rand(1000, 9999);
}
// 새 세금계산서 데이터 생성
$newInvoice = [
"id" => "inv_" . time(),
"issueKey" => $mgtKey, // MgtKey를 issueKey로 사용
"mgtKey" => $mgtKey, // 바로빌 관리번호
"supplierBizno" => $input['supplierBizno'] ?? '',
"supplierName" => $input['supplierName'] ?? '',
"recipientBizno" => $input['recipientBizno'] ?? '',
"recipientName" => $input['recipientName'] ?? '',
"supplyDate" => $input['supplyDate'] ?? date('Y-m-d'),
"items" => $input['items'] ?? [],
"totalSupplyAmt" => $input['totalSupplyAmt'] ?? 0,
"totalVat" => $input['totalVat'] ?? 0,
"total" => $input['total'] ?? 0,
"status" => "issued",
"memo" => $input['memo'] ?? '',
"createdAt" => date('Y-m-d\TH:i:s'),
"barobillInvoiceId" => is_numeric($apiData) ? (string)$apiData : ($apiData->InvoiceID ?? '')
];
// 파일에 저장
$dataFile = __DIR__ . '/invoices_data.json';
$existingData = [];
if (file_exists($dataFile)) {
$fileContent = file_get_contents($dataFile);
if ($fileContent !== false) {
$existingData = json_decode($fileContent, true);
if (!is_array($existingData)) {
$existingData = [];
}
}
}
if (!isset($existingData['invoices'])) {
$existingData['invoices'] = [];
}
$existingData['invoices'][] = $newInvoice;
// 최대 100개까지만 저장
if (count($existingData['invoices']) > 100) {
$existingData['invoices'] = array_slice($existingData['invoices'], -100);
}
// 파일 저장 시도 및 에러 확인
$jsonData = json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
$saveResult = @file_put_contents($dataFile, $jsonData);
$saveSuccess = ($saveResult !== false);
if (!$saveSuccess) {
$errorMsg = "파일 저장 실패: " . $dataFile;
if (!is_writable(dirname($dataFile))) {
$errorMsg .= " (디렉토리 쓰기 권한 없음)";
} elseif (!is_writable($dataFile) && file_exists($dataFile)) {
$errorMsg .= " (파일 쓰기 권한 없음)";
}
error_log("세금계산서 저장 실패: " . $errorMsg);
}
$response = [
"success" => true,
"message" => "세금계산서가 성공적으로 발행되었습니다.",
"data" => [
"issueKey" => $newInvoice['issueKey'],
"mgtKey" => $newInvoice['mgtKey'],
"barobillInvoiceId" => $newInvoice['barobillInvoiceId'],
"status" => "issued",
"issuedAt" => $newInvoice['createdAt']
],
"invoice" => $newInvoice,
"api_response" => $apiData,
"soap_request" => $apiResult['soap_request'] ?? null,
"soap_response" => $apiResult['soap_response'] ?? null,
"saved" => $saveSuccess,
"dataFile" => $saveSuccess ? null : $dataFile,
"simulation" => false,
"debug" => $debugInfo,
"note" => "✅ 실제 바로빌 API를 호출했습니다."
];
} else {
$response = [
"success" => false,
"error" => $apiResult['error'] ?? "API 호출 실패",
"error_code" => $apiResult['error_code'] ?? null,
"error_detail" => $apiResult['error_detail'] ?? null,
"soap_request" => $apiResult['soap_request'] ?? null,
"soap_response" => $apiResult['soap_response'] ?? null
];
}
} else {
// 시뮬레이션 모드 (API 키가 없을 때)
$issueKey = "BARO-" . date('Y') . "-" . str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT);
$newInvoice = [
"id" => "inv_" . time(),
"issueKey" => $issueKey,
"supplierBizno" => $input['supplierBizno'] ?? '',
"supplierName" => $input['supplierName'] ?? '',
"recipientBizno" => $input['recipientBizno'] ?? '',
"recipientName" => $input['recipientName'] ?? '',
"supplyDate" => $input['supplyDate'] ?? date('Y-m-d'),
"items" => $input['items'] ?? [],
"totalSupplyAmt" => $input['totalSupplyAmt'] ?? 0,
"totalVat" => $input['totalVat'] ?? 0,
"total" => $input['total'] ?? 0,
"status" => "issued",
"memo" => $input['memo'] ?? '',
"createdAt" => date('Y-m-d\TH:i:s'),
"barobillInvoiceId" => "BB-" . uniqid()
];
// 파일에 저장
$dataFile = __DIR__ . '/invoices_data.json';
$existingData = [];
if (file_exists($dataFile)) {
$fileContent = file_get_contents($dataFile);
if ($fileContent !== false) {
$existingData = json_decode($fileContent, true);
if (!is_array($existingData)) {
$existingData = [];
}
}
}
if (!isset($existingData['invoices'])) {
$existingData['invoices'] = [];
}
$existingData['invoices'][] = $newInvoice;
if (count($existingData['invoices']) > 100) {
$existingData['invoices'] = array_slice($existingData['invoices'], -100);
}
// 파일 저장 시도 및 에러 확인
$jsonData = json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
$saveResult = @file_put_contents($dataFile, $jsonData);
$saveSuccess = ($saveResult !== false);
if (!$saveSuccess) {
$errorMsg = "파일 저장 실패: " . $dataFile;
if (!is_writable(dirname($dataFile))) {
$errorMsg .= " (디렉토리 쓰기 권한 없음)";
} elseif (!is_writable($dataFile) && file_exists($dataFile)) {
$errorMsg .= " (파일 쓰기 권한 없음)";
}
error_log("세금계산서 저장 실패: " . $errorMsg);
}
$response = [
"success" => true,
"message" => "세금계산서가 성공적으로 발행되었습니다. (시뮬레이션 모드)",
"data" => [
"issueKey" => $issueKey,
"barobillInvoiceId" => $newInvoice['barobillInvoiceId'],
"status" => "issued",
"issuedAt" => $newInvoice['createdAt']
],
"invoice" => $newInvoice,
"simulation" => true,
"saved" => $saveSuccess,
"dataFile" => $saveSuccess ? null : $dataFile,
"debug" => $debugInfo,
"warning" => "⚠️ 시뮬레이션 모드입니다. 실제 바로빌 API를 호출하려면 CERTKEY를 설정하세요."
];
usleep(500000); // 0.5초 지연 시뮬레이션
}
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
?>

168
etax/api/status.php Normal file
View File

@@ -0,0 +1,168 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
// 바로빌 API 설정 로드
require_once(__DIR__ . '/barobill_config.php');
// POST 데이터 읽기
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['invoiceId'])) {
http_response_code(400);
echo json_encode([
"success" => false,
"error" => "Invoice ID is required"
], JSON_UNESCAPED_UNICODE);
exit;
}
$invoiceId = $input['invoiceId'];
// SOAP 클라이언트가 초기화되었고 CERTKEY가 있으면 실제 API 호출
$useRealAPI = !empty($barobillSoapClient) && !empty($barobillCertKey);
// 파일에서 데이터 읽기
$dataFile = __DIR__ . '/invoices_data.json';
$existingData = [];
if (file_exists($dataFile)) {
$existingData = json_decode(file_get_contents($dataFile), true);
if (!is_array($existingData)) {
$existingData = [];
}
}
// 기본 데이터에서도 찾기
$defaultInvoices = [
[
"id" => "inv_001",
"issueKey" => "BARO-2024-001",
"status" => "sent"
],
[
"id" => "inv_002",
"issueKey" => "BARO-2024-002",
"status" => "issued"
],
[
"id" => "inv_003",
"issueKey" => "BARO-2024-003",
"status" => "sent"
]
];
$allInvoices = array_merge($defaultInvoices, $existingData['invoices'] ?? []);
// 해당 세금계산서 찾기
$invoice = null;
foreach ($allInvoices as $inv) {
if ($inv['id'] === $invoiceId) {
$invoice = $inv;
break;
}
}
if (!$invoice) {
http_response_code(404);
echo json_encode([
"success" => false,
"error" => "Invoice not found"
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($useRealAPI && isset($invoice['issueKey'])) {
// 실제 바로빌 SOAP API 호출 - 국세청 전송
// issueKey를 MgtKey로 사용
$mgtKey = $invoice['mgtKey'] ?? $invoice['issueKey'];
$apiResult = sendToNTS($mgtKey);
if ($apiResult['success']) {
$apiData = $apiResult['data'];
// SOAP 응답에서 NTS 접수번호 추출
$ntsReceiptNo = '';
if (is_object($apiData) && isset($apiData->NTSConfirmNum)) {
$ntsReceiptNo = $apiData->NTSConfirmNum;
} elseif (is_array($apiData) && isset($apiData['NTSConfirmNum'])) {
$ntsReceiptNo = $apiData['NTSConfirmNum'];
}
// 상태 업데이트
$invoice['status'] = 'sent';
$invoice['ntsReceiptNo'] = $ntsReceiptNo;
$invoice['sentAt'] = date('Y-m-d\TH:i:s');
// 파일에 저장된 데이터 업데이트
if (isset($existingData['invoices'])) {
foreach ($existingData['invoices'] as &$inv) {
if ($inv['id'] === $invoiceId) {
$inv = $invoice;
break;
}
}
file_put_contents($dataFile, json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
$response = [
"success" => true,
"message" => "국세청 전송이 완료되었습니다.",
"data" => [
"issueKey" => $invoice['issueKey'] ?? '',
"mgtKey" => $mgtKey,
"ntsReceiptNo" => $invoice['ntsReceiptNo'],
"status" => "sent",
"sentAt" => $invoice['sentAt']
],
"invoice" => $invoice,
"api_response" => $apiData,
"soap_request" => $apiResult['soap_request'] ?? null,
"soap_response" => $apiResult['soap_response'] ?? null
];
} else {
$response = [
"success" => false,
"error" => $apiResult['error'] ?? "API 호출 실패",
"error_code" => $apiResult['error_code'] ?? null,
"error_detail" => $apiResult['error_detail'] ?? null,
"soap_request" => $apiResult['soap_request'] ?? null,
"soap_response" => $apiResult['soap_response'] ?? null
];
}
} else {
// 시뮬레이션 모드
$ntsReceiptNo = "NTS-" . date('Y') . "-" . str_pad(rand(100000, 999999), 6, '0', STR_PAD_LEFT);
$invoice['status'] = 'sent';
$invoice['ntsReceiptNo'] = $ntsReceiptNo;
$invoice['sentAt'] = date('Y-m-d\TH:i:s');
// 파일에 저장된 데이터 업데이트
if (isset($existingData['invoices'])) {
foreach ($existingData['invoices'] as &$inv) {
if ($inv['id'] === $invoiceId) {
$inv = $invoice;
break;
}
}
file_put_contents($dataFile, json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
usleep(800000); // 0.8초 지연 시뮬레이션
$response = [
"success" => true,
"message" => "국세청 전송이 완료되었습니다. (시뮬레이션 모드)",
"data" => [
"issueKey" => $invoice['issueKey'] ?? '',
"ntsReceiptNo" => $ntsReceiptNo,
"status" => "sent",
"sentAt" => $invoice['sentAt']
],
"invoice" => $invoice,
"simulation" => true
];
}
echo json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
?>

433
etax/barobill_api_info.php Normal file
View File

@@ -0,0 +1,433 @@
<?php
require_once __DIR__ . '/../lib/DotEnv.php';
(new DotEnv(__DIR__ . '/../.env'))->load();
require_once(getenv('DOCUMENT_ROOT') . "/session.php");
?>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>바로빌 API 정보</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 3px solid #43cea2;
}
.header h1 {
color: #185a9d;
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 1.2em;
}
.section {
margin-bottom: 40px;
}
.section h2 {
color: #185a9d;
font-size: 1.8em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #43cea2;
}
.section h3 {
color: #43cea2;
font-size: 1.4em;
margin: 20px 0 15px 0;
}
.info-box {
background: #f8f9fa;
border-left: 4px solid #43cea2;
padding: 20px;
margin: 20px 0;
border-radius: 5px;
}
.info-box.warning {
border-left-color: #f5576c;
background: #fff5f5;
}
.info-box.success {
border-left-color: #43cea2;
background: #f0fdf4;
}
.api-endpoint {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
font-family: 'Courier New', monospace;
overflow-x: auto;
}
.api-endpoint .method {
color: #4ec9b0;
font-weight: bold;
}
.api-endpoint .url {
color: #ce9178;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
table th,
table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
table th {
background: #43cea2;
color: white;
font-weight: 600;
}
table tr:hover {
background: #f5f5f5;
}
.btn {
display: inline-block;
padding: 12px 30px;
background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%);
color: white;
text-decoration: none;
border-radius: 25px;
font-weight: 600;
transition: all 0.3s ease;
margin: 10px 10px 10px 0;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.btn-secondary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
ul, ol {
margin-left: 30px;
line-height: 2;
}
li {
margin-bottom: 10px;
}
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: #e83e8c;
}
.home-btn {
position: fixed;
top: 30px;
left: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 25px;
border-radius: 50px;
cursor: pointer;
font-size: 1em;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
z-index: 1000;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.home-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.header h1 {
font-size: 1.8em;
}
.home-btn {
top: 15px;
left: 15px;
padding: 10px 20px;
font-size: 0.9em;
}
}
</style>
</head>
<body>
<!-- Home Button -->
<a href="../index.php" class="home-btn">
<span>🏠</span>
<span>홈으로</span>
</a>
<div class="container">
<div class="header">
<h1>📋 바로빌 API 정보</h1>
<p>전자세금계산서 솔루션 개발을 위한 바로빌 API 가이드</p>
</div>
<!-- 바로빌 개요 -->
<div class="section">
<h2>1. 바로빌 개요</h2>
<div class="info-box success">
<p><strong>바로빌</strong>은 기업의 재무회계 디지털 전환을 지원하는 Fin-tech 플랫폼입니다.</p>
<p>다양한 전자문서 발행 및 관리 API를 제공하여 기업의 업무 자동화를 지원합니다.</p>
</div>
<p><strong>개발자센터 URL</strong>: <a href="https://dev.barobill.co.kr" target="_blank">https://dev.barobill.co.kr</a></p>
</div>
<!-- 주요 API 서비스 -->
<div class="section">
<h2>2. 주요 API 서비스</h2>
<h3>2.1 전자문서 API</h3>
<p>전자세금계산서, 거래명세서, 청구서, 견적서 등 다양한 전자문서의 발행 및 관리 기능을 제공합니다.</p>
<ul>
<li><strong>전자세금계산서 발행</strong>: 세금계산서 발행 및 국세청 전송</li>
<li><strong>전자세금계산서 취소</strong>: 발행된 세금계산서 취소</li>
<li><strong>전자세금계산서 조회</strong>: 발행 내역 및 상태 조회</li>
<li><strong>거래명세서 발행</strong>: 거래명세서 발행 및 관리</li>
<li><strong>청구서/견적서 발행</strong>: 다양한 전자문서 발행</li>
</ul>
<p><a href="https://dev.barobill.co.kr/services/edoc" target="_blank" class="btn">전자문서 API 상세보기</a></p>
<h3>2.2 메시징 API</h3>
<p>SMS, LMS, MMS 등 다양한 형태의 문자 전송 기능을 제공합니다.</p>
<ul>
<li>세금계산서 발행 알림</li>
<li>인증서 만료 알림</li>
<li>시스템 알림</li>
</ul>
<p><a href="https://dev.barobill.co.kr/services/message" target="_blank" class="btn">메시징 API 상세보기</a></p>
<h3>2.3 팩스 전송 API</h3>
<p>HWP, DOC, XLS, PDF 등 다양한 문서 파일의 팩스 전송 기능을 제공합니다.</p>
<p><a href="https://dev.barobill.co.kr/services/fax" target="_blank" class="btn">팩스 API 상세보기</a></p>
</div>
<!-- 전자세금계산서 API 상세 -->
<div class="section">
<h2>3. 전자세금계산서 API 상세</h2>
<h3>3.1 필수 요구사항</h3>
<div class="info-box warning">
<p><strong>⚠️ 중요:</strong> 전자세금계산서 발행을 위해서는 공동인증서 또는 금융인증서가 필요합니다.</p>
</div>
<ul>
<li>공동인증서 또는 금융인증서 등록</li>
<li>바로빌 개발자센터 회원가입</li>
<li>API 키 발급</li>
<li>인증서 등록 및 연동</li>
</ul>
<h3>3.2 주요 API 기능</h3>
<table>
<thead>
<tr>
<th>기능</th>
<th>설명</th>
<th>필수 정보</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>세금계산서 발행</strong></td>
<td>전자세금계산서 발행 및 국세청 전송</td>
<td>공급자/수취자 정보, 품목 정보, 금액 정보</td>
</tr>
<tr>
<td><strong>세금계산서 취소</strong></td>
<td>발행된 세금계산서 취소</td>
<td>발행 키, 취소 사유</td>
</tr>
<tr>
<td><strong>세금계산서 조회</strong></td>
<td>발행 내역 및 상태 조회</td>
<td>발행 키 또는 검색 조건</td>
</tr>
<tr>
<td><strong>국세청 전송</strong></td>
<td>발행된 세금계산서를 국세청으로 전송</td>
<td>발행 키</td>
</tr>
<tr>
<td><strong>전송 상태 확인</strong></td>
<td>국세청 전송 상태 및 접수번호 확인</td>
<td>발행 키</td>
</tr>
</tbody>
</table>
<h3>3.3 API 요청 예시 (예상)</h3>
<div class="api-endpoint">
<span class="method">POST</span> <span class="url">/api/taxinvoice/issue</span>
</div>
<p><strong>Headers:</strong></p>
<div class="api-endpoint">
Authorization: Bearer {API_KEY}<br>
Content-Type: application/json
</div>
<p><strong>Request Body (예상):</strong></p>
<div class="api-endpoint">
{<br>
&nbsp;&nbsp;"certId": "인증서ID",<br>
&nbsp;&nbsp;"supplier": {<br>
&nbsp;&nbsp;&nbsp;&nbsp;"bizno": "사업자번호",<br>
&nbsp;&nbsp;&nbsp;&nbsp;"corpName": "상호",<br>
&nbsp;&nbsp;&nbsp;&nbsp;"ceo": "대표자명"<br>
&nbsp;&nbsp;},<br>
&nbsp;&nbsp;"recipient": { ... },<br>
&nbsp;&nbsp;"items": [ ... ],<br>
&nbsp;&nbsp;"writeDate": "2024-01-01",<br>
&nbsp;&nbsp;"supplyDate": "2024-01-01"<br>
}
</div>
</div>
<!-- 개발자 지원 -->
<div class="section">
<h2>4. 개발자 지원</h2>
<ul>
<li><strong>개발 가이드</strong>: 상세한 API 문서 및 가이드 제공</li>
<li><strong>샘플 코드</strong>: Java, Python, PHP, .NET 등 다양한 언어 지원</li>
<li><strong>테스트 환경</strong>: 실제 개발 환경과 동일한 테스트 환경 제공</li>
<li><strong>전담 엔지니어 지원</strong>: 개발 상담 및 기술 지원 제공</li>
</ul>
<p><a href="https://dev.barobill.co.kr" target="_blank" class="btn">개발자센터 바로가기</a></p>
</div>
<!-- 연동 절차 -->
<div class="section">
<h2>5. API 연동 절차</h2>
<ol>
<li><strong>회원가입</strong>: 바로빌 개발자센터에서 회원가입</li>
<li><strong>API 키 발급</strong>: 개발자센터에서 API 키 발급</li>
<li><strong>인증서 등록</strong>: 공동인증서 또는 금융인증서 등록</li>
<li><strong>테스트 환경 설정</strong>: 바로빌 테스트 환경에서 연동 테스트</li>
<li><strong>개발 및 연동</strong>: API 가이드를 참고하여 연동 및 개발 진행</li>
<li><strong>프로덕션 연동</strong>: 실제 운영 환경 구축 후 서비스 오픈</li>
</ol>
</div>
<!-- 요금 안내 -->
<div class="section">
<h2>6. 요금 안내</h2>
<div class="info-box">
<p>바로빌은 파트너 유형에 따라 할인형과 수익형으로 구분하여 연동 서비스를 제공합니다.</p>
<p>자세한 요금 안내는 바로빌 개발자센터의 파트너/요금 페이지를 참고하시기 바랍니다.</p>
</div>
<p><a href="https://dev.barobill.co.kr/partners/cost/partner" target="_blank" class="btn btn-secondary">요금 안내 보기</a></p>
</div>
<!-- 지원 언어 -->
<div class="section">
<h2>7. 지원 언어</h2>
<p>바로빌 API는 다음과 같은 개발 언어를 지원합니다:</p>
<ul>
<li>Java</li>
<li>Python</li>
<li>PHP</li>
<li>.NET (C#)</li>
<li>기타 REST API를 지원하는 모든 언어</li>
</ul>
</div>
<!-- 보안 고려사항 -->
<div class="section">
<h2>8. 보안 고려사항</h2>
<div class="info-box warning">
<p><strong>⚠️ 보안 주의사항:</strong></p>
<ul>
<li>API 키는 절대 공개되지 않도록 주의하세요.</li>
<li>인증서는 암호화하여 안전하게 저장하세요.</li>
<li>HTTPS를 통해서만 API를 호출하세요.</li>
<li>민감한 정보는 로그에 기록하지 마세요.</li>
</ul>
</div>
</div>
<!-- 유용한 링크 -->
<div class="section">
<h2>9. 유용한 링크</h2>
<ul>
<li><a href="https://dev.barobill.co.kr" target="_blank">바로빌 개발자센터</a></li>
<li><a href="https://dev.barobill.co.kr/services/edoc" target="_blank">전자문서 API</a></li>
<li><a href="https://dev.barobill.co.kr/services/message" target="_blank">메시징 API</a></li>
<li><a href="https://dev.barobill.co.kr/services/fax" target="_blank">팩스 API</a></li>
<li><a href="https://dev.barobill.co.kr/partners/cost/partner" target="_blank">파트너/요금 안내</a></li>
</ul>
</div>
<!-- 다음 단계 -->
<div class="section">
<h2>10. 다음 단계</h2>
<ol>
<li>바로빌 개발자센터 회원가입 및 API 키 발급</li>
<li>개발 가이드 및 샘플 코드 다운로드</li>
<li>테스트 환경에서 API 연동 테스트</li>
<li>인증서 등록 및 연동</li>
<li>실제 프로젝트에 통합</li>
</ol>
<p><a href="../strategy/electronicTaxInvoice_index.php" class="btn">전략 페이지로 돌아가기</a></p>
</div>
</div>
</body>
</html>

27
etax/dev.md Normal file
View File

@@ -0,0 +1,27 @@
# Etax 개발 노트
## API 오류 해결 가이드
### 바로빌 SOAP 클라이언트 미설치 오류 (500 Error)
**문제 상황:**
서버에 PHP SOAP 확장 모듈이 설치되어 있지 않은 경우(`Class 'SoapClient' not found`), `new SoapClient()` 호출 시 치명적인 오류(Fatal Error)가 발생하여 HTTP 500 상태 코드를 반환합니다.
**해결 방법:**
`soapClient` 생성 로직을 `try-catch` 블록으로 감싸되, `Exception`이 아닌 **`Throwable`**을 catch해야 합니다. PHP 7+에서는 치명적인 오류가 `Error` 객체로 던져지며, 이는 `Exception`이 아닌 `Throwable` 인터페이스를 구현하기 때문입니다.
**수정 예시:**
```php
$barobillSoapClient = null;
try {
$barobillSoapClient = new SoapClient($url, $options);
} catch (Throwable $e) {
// Class not found 등의 Fatal Error도 여기서 잡힘
error_log('SOAP Client 생성 실패: ' . $e->getMessage());
// 이후 로직에서 $barobillSoapClient가 null일 경우의 대체 로직(예: 시뮬레이션 모드) 수행
}
```
**적용 파일:**
- `etax/api/barobill_config.php`
- `etax/api/issue.php` (전역 에러 핸들링)

1086
etax/index.php Normal file

File diff suppressed because it is too large Load Diff

45
lib/DotEnv.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
if (!class_exists('DotEnv')) {
class DotEnv
{
/**
* The directory where the .env file is located.
*
* @var string
*/
protected $path;
public function __construct(string $path)
{
if (!file_exists($path)) {
throw new \InvalidArgumentException(sprintf('%s does not exist', $path));
}
$this->path = $path;
}
public function load(): void
{
if (!is_readable($this->path)) {
throw new \RuntimeException(sprintf('%s file is not readable', $this->path));
}
$lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) {
putenv(sprintf('%s=%s', $name, $value));
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}
}
}
}

45
lib/func.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
session_start();
?>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="./css/common.css">
<link rel="stylesheet" type="text/css" href="./css/func.css">
</head>
<?php
function latest_article($table,$loop,$char_limit)
{
require_once("../lib/mydb.php");
$pdo=db_connect();
try{
$sql="select * from chandj.$table order by num desc limit $loop";
$stmh=$pdo->query($sql);
While($row=$stmh->fetch(PDO::FETCH_ASSOC))
{
$num=$row["num"];
$len_subject=strlen($row["subject"]);
$subject=$row["subject"];
if($len_subject>$char_limit)
{
$subject=mb_substr($row["subject"],0,$char_limit,'utf-8');
$subject=$subject . "..."; // 글자수가 초과하면 ...으로 표기됨
}
$regist_day=substr($row["regist_day"],0,10);
$page=1;
echo("<div class='col1'> <a href='./$table/view.php?num=$num&page=$page'>$subject</a>
</div><div class='col2'>$regist_day</div>
<div class='clear'></div>");
}
} catch (PDOException $Exception) {
print "오류: ". $Exception->getMessage();
}
}
?>

21
lib/helper.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
function href($url)
{
print "
<script>
location.href = '$url';
</script>
";
}
function alert($msg)
{
print "
<script>
alert('$msg');
</script>
";
}
?>