B2B 이커머스 세금계산서 자동화: 국세청 API 연동과 계산서 발행 시스템

이커머스

세금계산서B2B 이커머스전자세금계산서국세청 API회계 자동화

이 글은 누구를 위한 것인가

  • B2B 주문이 늘면서 세금계산서 발행이 수동으로 힘든 팀
  • 국세청 API 연동 방법을 모르는 개발자
  • 월말 세금계산서 일괄 발행으로 CS가 쌓이는 운영팀

들어가며

B2B 이커머스에서 세금계산서는 필수다. 수동 발행은 오류가 많고 인력이 많이 든다. 국세청 전자세금계산서 API를 연동하면 주문 완료 즉시 자동 발행이 가능하다.

이 글은 bluefoxdev.kr의 B2B 이커머스 시스템 설계 를 참고하여 작성했습니다.


1. 전자세금계산서 발행 흐름

[세금계산서 자동 발행 흐름]

주문 완료
  ↓
사업자 정보 확인
  ↓ (사업자 주문인 경우)
사업자번호 유효성 검증 (국세청 API)
  ↓
전자세금계산서 발행 (공급자 → 공급받는자)
  ↓
국세청 전송 (발행일 익일 17시까지)
  ↓
이메일 발송 (고객 + 사본 보관)
  ↓
회계 시스템 연동 (ERP)

[세금계산서 DB 스키마]
CREATE TABLE tax_invoices (
    id UUID PRIMARY KEY,
    order_id UUID NOT NULL,
    invoice_number VARCHAR(24) UNIQUE,    -- 일련번호 24자리
    supplier_biz_num VARCHAR(10) NOT NULL, -- 공급자 사업자번호
    buyer_biz_num VARCHAR(10) NOT NULL,    -- 공급받는자 사업자번호
    issue_date DATE NOT NULL,
    supply_amount BIGINT NOT NULL,         -- 공급가액
    tax_amount BIGINT NOT NULL,            -- 세액
    total_amount BIGINT NOT NULL,          -- 합계금액
    status VARCHAR(20) DEFAULT 'issued',   -- issued, sent, cancelled, amended
    nts_id VARCHAR(50),                    -- 국세청 승인번호
    sent_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

2. 국세청 API 연동

import httpx
from datetime import date
import hashlib
import hmac

class NTSInvoiceClient:
    """국세청 전자세금계산서 API 클라이언트"""
    
    BASE_URL = "https://api.hometax.go.kr/etax"
    
    def __init__(self, cert_path: str, cert_password: str):
        self.cert_path = cert_path
        self.cert_password = cert_password
    
    async def validate_business_number(self, biz_num: str) -> dict:
        """사업자번호 유효성 검증"""
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.BASE_URL}/validate/business",
                json={"business_number": biz_num},
                headers=self._get_auth_headers(),
            )
        return response.json()
    
    async def issue_invoice(self, invoice_data: dict) -> dict:
        """전자세금계산서 발행"""
        payload = {
            "invoiceType": "01",  # 일반 세금계산서
            "issueDate": invoice_data["issue_date"],
            "supplier": {
                "businessNumber": invoice_data["supplier_biz_num"],
                "businessName": invoice_data["supplier_name"],
                "ceoName": invoice_data["supplier_ceo"],
                "email": invoice_data["supplier_email"],
            },
            "buyer": {
                "businessNumber": invoice_data["buyer_biz_num"],
                "businessName": invoice_data["buyer_name"],
                "email": invoice_data["buyer_email"],
            },
            "items": [
                {
                    "name": item["name"],
                    "quantity": item["quantity"],
                    "unitPrice": item["unit_price"],
                    "supplyAmount": item["supply_amount"],
                    "taxAmount": item["tax_amount"],
                }
                for item in invoice_data["items"]
            ],
            "totalSupplyAmount": invoice_data["supply_amount"],
            "totalTaxAmount": invoice_data["tax_amount"],
            "totalAmount": invoice_data["total_amount"],
        }
        
        async with httpx.AsyncClient(cert=(self.cert_path, self.cert_password)) as client:
            response = await client.post(
                f"{self.BASE_URL}/issue",
                json=payload,
            )
        
        return response.json()
    
    def _get_auth_headers(self) -> dict:
        timestamp = str(int(datetime.now().timestamp()))
        return {
            "X-NTS-Timestamp": timestamp,
            "X-NTS-Signature": self._sign(timestamp),
        }

async def auto_issue_tax_invoice(order_id: str):
    """주문 완료 시 세금계산서 자동 발행"""
    
    order = await get_order_with_buyer_info(order_id)
    
    # 사업자 주문이 아니면 스킵
    if not order.get("buyer_biz_num"):
        return
    
    # 사업자번호 검증
    nts_client = NTSInvoiceClient(CERT_PATH, CERT_PASSWORD)
    validation = await nts_client.validate_business_number(order["buyer_biz_num"])
    
    if not validation["valid"]:
        await notify_ops_team(f"유효하지 않은 사업자번호: {order['buyer_biz_num']}")
        return
    
    # 세금 계산
    supply_amount = round(order["total_amount"] / 1.1)  # VAT 역산
    tax_amount = order["total_amount"] - supply_amount
    
    invoice_data = {
        "issue_date": date.today().isoformat(),
        "supplier_biz_num": MY_BUSINESS_NUMBER,
        "buyer_biz_num": order["buyer_biz_num"],
        "buyer_name": order["company_name"],
        "buyer_email": order["billing_email"],
        "supply_amount": supply_amount,
        "tax_amount": tax_amount,
        "total_amount": order["total_amount"],
        "items": build_invoice_items(order["items"]),
    }
    
    result = await nts_client.issue_invoice(invoice_data)
    
    # DB 저장
    await save_tax_invoice(order_id, result)
    
    # 이메일 발송
    await send_invoice_email(order["billing_email"], result["invoice_pdf_url"])

3. 수정 세금계산서 처리

async def issue_amended_invoice(original_invoice_id: str, reason: str):
    """수정 세금계산서 발행 (환불, 취소 시)"""
    
    original = await get_tax_invoice(original_invoice_id)
    
    # 음수 금액으로 수정 계산서 발행
    amended_data = {
        **original,
        "invoiceType": "02",  # 수정 세금계산서
        "originalInvoiceId": original["nts_id"],
        "amendReason": reason,
        "supply_amount": -original["supply_amount"],
        "tax_amount": -original["tax_amount"],
        "total_amount": -original["total_amount"],
    }
    
    nts_client = NTSInvoiceClient(CERT_PATH, CERT_PASSWORD)
    result = await nts_client.issue_invoice(amended_data)
    
    # 원본 계산서 상태 업데이트
    await db.execute(
        "UPDATE tax_invoices SET status='amended' WHERE id=$1",
        original_invoice_id
    )
    
    return result

마무리

전자세금계산서 자동화의 핵심은 주문 완료 직후 즉시 발행이다. 월말에 몰아서 처리하면 오류가 생기고, 고객도 불편하다. 사업자번호 검증을 주문 단계에서 하면 발행 실패를 사전에 막을 수 있다.