diff --git a/apikey/barobill_api_key.txt b/apikey/barobill_api_key.txt new file mode 100644 index 0000000..af2f171 --- /dev/null +++ b/apikey/barobill_api_key.txt @@ -0,0 +1 @@ +2DD6C76C-04DB-44F7-B6E9-3FC0B2211826 \ No newline at end of file diff --git a/apikey/barobill_api_key.txt.example b/apikey/barobill_api_key.txt.example new file mode 100644 index 0000000..af2f171 --- /dev/null +++ b/apikey/barobill_api_key.txt.example @@ -0,0 +1 @@ +2DD6C76C-04DB-44F7-B6E9-3FC0B2211826 \ No newline at end of file diff --git a/apikey/barobill_cert_id.txt b/apikey/barobill_cert_id.txt new file mode 100644 index 0000000..61a4c04 --- /dev/null +++ b/apikey/barobill_cert_id.txt @@ -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에서는 가상의 인증서를 사용할 수 있습니다 diff --git a/apikey/barobill_cert_id.txt.example b/apikey/barobill_cert_id.txt.example new file mode 100644 index 0000000..a2061e7 --- /dev/null +++ b/apikey/barobill_cert_id.txt.example @@ -0,0 +1,3 @@ +여기에 바로빌 인증서 ID를 입력하세요 (선택사항) +예: cert-id-here + diff --git a/apikey/barobill_cert_key.txt b/apikey/barobill_cert_key.txt new file mode 100644 index 0000000..7e57088 --- /dev/null +++ b/apikey/barobill_cert_key.txt @@ -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가 필요할 수 있습니다 + diff --git a/apikey/barobill_corp_num.txt b/apikey/barobill_corp_num.txt new file mode 100644 index 0000000..7c0a821 --- /dev/null +++ b/apikey/barobill_corp_num.txt @@ -0,0 +1 @@ +6648603713 \ No newline at end of file diff --git a/apikey/barobill_test_mode.txt b/apikey/barobill_test_mode.txt new file mode 100644 index 0000000..f32a580 --- /dev/null +++ b/apikey/barobill_test_mode.txt @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/apikey/barobill_test_mode.txt.example b/apikey/barobill_test_mode.txt.example new file mode 100644 index 0000000..f32a580 --- /dev/null +++ b/apikey/barobill_test_mode.txt.example @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/apikey/barobill_user_id.txt b/apikey/barobill_user_id.txt new file mode 100644 index 0000000..70bfd2d --- /dev/null +++ b/apikey/barobill_user_id.txt @@ -0,0 +1 @@ +cbx0913 diff --git a/apikey/claude_api.txt b/apikey/claude_api.txt new file mode 100644 index 0000000..48370e8 --- /dev/null +++ b/apikey/claude_api.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/apikey/gcs_config.txt b/apikey/gcs_config.txt new file mode 100644 index 0000000..9667bbc --- /dev/null +++ b/apikey/gcs_config.txt @@ -0,0 +1,2 @@ +bucket_name=codebridge-speech-audio-files + diff --git a/apikey/gemini_api_key.txt b/apikey/gemini_api_key.txt new file mode 100644 index 0000000..85fa17f --- /dev/null +++ b/apikey/gemini_api_key.txt @@ -0,0 +1 @@ +AIzaSyAS3bAzmXlhhZHgO3buFiTGzavXZ6ubYq8 \ No newline at end of file diff --git a/apikey/google_api.txt b/apikey/google_api.txt new file mode 100644 index 0000000..39f4a79 --- /dev/null +++ b/apikey/google_api.txt @@ -0,0 +1 @@ +f7d58533aa1dba0db19d799d85f22686684521d2 \ No newline at end of file diff --git a/apikey/google_service_account.json b/apikey/google_service_account.json new file mode 100644 index 0000000..227553c --- /dev/null +++ b/apikey/google_service_account.json @@ -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" + } \ No newline at end of file diff --git a/apikey/google_vertex_api.txt b/apikey/google_vertex_api.txt new file mode 100644 index 0000000..85fa17f --- /dev/null +++ b/apikey/google_vertex_api.txt @@ -0,0 +1 @@ +AIzaSyAS3bAzmXlhhZHgO3buFiTGzavXZ6ubYq8 \ No newline at end of file diff --git a/apikey/notion.txt b/apikey/notion.txt new file mode 100644 index 0000000..9e9d582 --- /dev/null +++ b/apikey/notion.txt @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/apikey/opendart.txt b/apikey/opendart.txt new file mode 100644 index 0000000..a4a0890 --- /dev/null +++ b/apikey/opendart.txt @@ -0,0 +1 @@ +ad5002bf2c7d2f93eac3c6f9ff1d4b63bf3027bb \ No newline at end of file diff --git a/barobill_registration/index.php b/barobill_registration/index.php index a7a1bd5..439dbf6 100644 --- a/barobill_registration/index.php +++ b/barobill_registration/index.php @@ -60,7 +60,7 @@ 현황 - + 세금계산서 diff --git a/etax/README_DB.md b/etax/README_DB.md new file mode 100644 index 0000000..a8e34ed --- /dev/null +++ b/etax/README_DB.md @@ -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`)에서 데이터를 마이그레이션하려면 별도의 마이그레이션 스크립트를 작성해야 합니다. + diff --git a/etax/api/API_URL_GUIDE.md b/etax/api/API_URL_GUIDE.md new file mode 100644 index 0000000..2c92e24 --- /dev/null +++ b/etax/api/API_URL_GUIDE.md @@ -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 + diff --git a/etax/api/README.md b/etax/api/README.md new file mode 100644 index 0000000..4b3bfbd --- /dev/null +++ b/etax/api/README.md @@ -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가 선택사항일 수 있음 + +### 사업자번호 오류 + +- 하이픈(-) 없이 숫자만 입력했는지 확인 +- 발행자 사업자번호가 올바른지 확인 diff --git a/etax/api/barobill_config.php b/etax/api/barobill_config.php new file mode 100644 index 0000000..c52547f --- /dev/null +++ b/etax/api/barobill_config.php @@ -0,0 +1,418 @@ +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); +} + +?> + diff --git a/etax/api/debug_test.php b/etax/api/debug_test.php new file mode 100644 index 0000000..5c7efc3 --- /dev/null +++ b/etax/api/debug_test.php @@ -0,0 +1,34 @@ +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"; diff --git a/etax/api/delete.php b/etax/api/delete.php new file mode 100644 index 0000000..3d523ed --- /dev/null +++ b/etax/api/delete.php @@ -0,0 +1,163 @@ + 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); +?> + diff --git a/etax/api/deleted_ids.json b/etax/api/deleted_ids.json new file mode 100644 index 0000000..4723510 --- /dev/null +++ b/etax/api/deleted_ids.json @@ -0,0 +1,5 @@ +[ + "inv_001", + "inv_002", + "inv_003" +] \ No newline at end of file diff --git a/etax/api/invoices.php b/etax/api/invoices.php new file mode 100644 index 0000000..89eeabe --- /dev/null +++ b/etax/api/invoices.php @@ -0,0 +1,170 @@ + "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); +?> + diff --git a/etax/api/invoices_data.json b/etax/api/invoices_data.json new file mode 100644 index 0000000..a17f63c --- /dev/null +++ b/etax/api/invoices_data.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/etax/api/issue.php b/etax/api/issue.php new file mode 100644 index 0000000..e576dc7 --- /dev/null +++ b/etax/api/issue.php @@ -0,0 +1,226 @@ + 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); +?> + diff --git a/etax/api/status.php b/etax/api/status.php new file mode 100644 index 0000000..cd8e935 --- /dev/null +++ b/etax/api/status.php @@ -0,0 +1,168 @@ + 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); +?> + diff --git a/etax/barobill_api_info.php b/etax/barobill_api_info.php new file mode 100644 index 0000000..762f2ad --- /dev/null +++ b/etax/barobill_api_info.php @@ -0,0 +1,433 @@ +load(); +require_once(getenv('DOCUMENT_ROOT') . "/session.php"); +?> + + + + + + 바로빌 API 정보 + + + + + + 🏠 + 홈으로 + + +
+
+

📋 바로빌 API 정보

+

전자세금계산서 솔루션 개발을 위한 바로빌 API 가이드

+
+ + +
+

1. 바로빌 개요

+
+

바로빌은 기업의 재무회계 디지털 전환을 지원하는 Fin-tech 플랫폼입니다.

+

다양한 전자문서 발행 및 관리 API를 제공하여 기업의 업무 자동화를 지원합니다.

+
+

개발자센터 URL: https://dev.barobill.co.kr

+
+ + +
+

2. 주요 API 서비스

+ +

2.1 전자문서 API

+

전자세금계산서, 거래명세서, 청구서, 견적서 등 다양한 전자문서의 발행 및 관리 기능을 제공합니다.

+ +

전자문서 API 상세보기

+ +

2.2 메시징 API

+

SMS, LMS, MMS 등 다양한 형태의 문자 전송 기능을 제공합니다.

+ +

메시징 API 상세보기

+ +

2.3 팩스 전송 API

+

HWP, DOC, XLS, PDF 등 다양한 문서 파일의 팩스 전송 기능을 제공합니다.

+

팩스 API 상세보기

+
+ + +
+

3. 전자세금계산서 API 상세

+ +

3.1 필수 요구사항

+
+

⚠️ 중요: 전자세금계산서 발행을 위해서는 공동인증서 또는 금융인증서가 필요합니다.

+
+ + +

3.2 주요 API 기능

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
기능설명필수 정보
세금계산서 발행전자세금계산서 발행 및 국세청 전송공급자/수취자 정보, 품목 정보, 금액 정보
세금계산서 취소발행된 세금계산서 취소발행 키, 취소 사유
세금계산서 조회발행 내역 및 상태 조회발행 키 또는 검색 조건
국세청 전송발행된 세금계산서를 국세청으로 전송발행 키
전송 상태 확인국세청 전송 상태 및 접수번호 확인발행 키
+ +

3.3 API 요청 예시 (예상)

+
+ POST /api/taxinvoice/issue +
+

Headers:

+
+Authorization: Bearer {API_KEY}
+Content-Type: application/json +
+

Request Body (예상):

+
+{
+  "certId": "인증서ID",
+  "supplier": {
+    "bizno": "사업자번호",
+    "corpName": "상호",
+    "ceo": "대표자명"
+  },
+  "recipient": { ... },
+  "items": [ ... ],
+  "writeDate": "2024-01-01",
+  "supplyDate": "2024-01-01"
+} +
+
+ + +
+

4. 개발자 지원

+ +

개발자센터 바로가기

+
+ + +
+

5. API 연동 절차

+
    +
  1. 회원가입: 바로빌 개발자센터에서 회원가입
  2. +
  3. API 키 발급: 개발자센터에서 API 키 발급
  4. +
  5. 인증서 등록: 공동인증서 또는 금융인증서 등록
  6. +
  7. 테스트 환경 설정: 바로빌 테스트 환경에서 연동 테스트
  8. +
  9. 개발 및 연동: API 가이드를 참고하여 연동 및 개발 진행
  10. +
  11. 프로덕션 연동: 실제 운영 환경 구축 후 서비스 오픈
  12. +
+
+ + +
+

6. 요금 안내

+
+

바로빌은 파트너 유형에 따라 할인형과 수익형으로 구분하여 연동 서비스를 제공합니다.

+

자세한 요금 안내는 바로빌 개발자센터의 파트너/요금 페이지를 참고하시기 바랍니다.

+
+

요금 안내 보기

+
+ + +
+

7. 지원 언어

+

바로빌 API는 다음과 같은 개발 언어를 지원합니다:

+ +
+ + +
+

8. 보안 고려사항

+
+

⚠️ 보안 주의사항:

+
    +
  • API 키는 절대 공개되지 않도록 주의하세요.
  • +
  • 인증서는 암호화하여 안전하게 저장하세요.
  • +
  • HTTPS를 통해서만 API를 호출하세요.
  • +
  • 민감한 정보는 로그에 기록하지 마세요.
  • +
+
+
+ + +
+

9. 유용한 링크

+ +
+ + +
+

10. 다음 단계

+
    +
  1. 바로빌 개발자센터 회원가입 및 API 키 발급
  2. +
  3. 개발 가이드 및 샘플 코드 다운로드
  4. +
  5. 테스트 환경에서 API 연동 테스트
  6. +
  7. 인증서 등록 및 연동
  8. +
  9. 실제 프로젝트에 통합
  10. +
+

전략 페이지로 돌아가기

+
+
+ + + diff --git a/etax/dev.md b/etax/dev.md new file mode 100644 index 0000000..8669ca5 --- /dev/null +++ b/etax/dev.md @@ -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` (전역 에러 핸들링) diff --git a/etax/index.php b/etax/index.php new file mode 100644 index 0000000..1e30649 --- /dev/null +++ b/etax/index.php @@ -0,0 +1,1086 @@ + + + + + + 전자세금계산서 솔루션 - 바로빌 연동 + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/lib/DotEnv.php b/lib/DotEnv.php new file mode 100644 index 0000000..c2a547a --- /dev/null +++ b/lib/DotEnv.php @@ -0,0 +1,45 @@ +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; + } + } + } + } +} diff --git a/lib/func.php b/lib/func.php new file mode 100644 index 0000000..e58d1d7 --- /dev/null +++ b/lib/func.php @@ -0,0 +1,45 @@ + + + + + + + + + +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("
$subject +
$regist_day
+
"); + + } + } catch (PDOException $Exception) { + print "오류: ". $Exception->getMessage(); + + } + } + ?> \ No newline at end of file diff --git a/lib/helper.php b/lib/helper.php new file mode 100644 index 0000000..f3eb0cd --- /dev/null +++ b/lib/helper.php @@ -0,0 +1,21 @@ + + location.href = '$url'; + + "; +} + +function alert($msg) +{ + print " + + "; +} + +?> \ No newline at end of file