B2B 이커머스 설계: 기업 구매 워크플로우와 견적·승인 시스템 구현 가이드

커머스

B2B 이커머스견적 시스템구매 승인기업 구매이커머스 설계

이 글은 누구를 위한 것인가

  • B2C 이커머스에 B2B 기능을 추가하거나 B2B 전용 플랫폼을 구축하는 개발자
  • 기업 고객의 복잡한 구매 프로세스를 온라인으로 전환하려는 PM
  • 대량 구매 견적 및 계약 관리 시스템 설계가 필요한 팀

들어가며

B2B 이커머스는 B2C와 근본적으로 다르다. B2C는 개인이 즉시 결정하고 결제하지만, B2B는 구매 부서 승인, 예산 확인, 세금계산서 처리, 계약 조건 협상이 따른다. 한 건의 주문이 수일에서 수주가 걸릴 수 있다.

그러나 이 복잡성을 온라인으로 잘 구현하면 기업 고객의 구매 효율을 크게 높이고 고객 락인 효과도 생긴다. 아마존 비즈니스가 연간 수조 원 규모로 성장한 이유가 여기에 있다.

이 글은 bluefoxdev.kr의 B2B 디지털 전환 전략 을 참고하고, 시스템 구현 관점에서 확장하여 작성했습니다.


1. B2B 계정 구조 설계

1.1 계정 계층 모델

interface Company {
  id: string;
  name: string;
  businessNumber: string;  // 사업자등록번호
  tier: 'STANDARD' | 'PREFERRED' | 'ENTERPRISE';
  creditLimit: number;     // 외상 한도
  paymentTerms: PaymentTerms;
  assignedSalesRep?: string;
}

interface CompanyUser {
  id: string;
  companyId: string;
  email: string;
  role: 'ADMIN' | 'PURCHASER' | 'APPROVER' | 'VIEWER';
  purchaseLimit?: number;  // 개인별 주문 한도
  approvalGroups: string[]; // 소속 승인 그룹
}

interface PaymentTerms {
  type: 'IMMEDIATE' | 'NET_30' | 'NET_60' | 'NET_90';
  invoiceType: 'TAX_INVOICE' | 'CARD' | 'TRANSFER';
}

1.2 B2B 전용 가격 정책

class B2BPricingService {
  async getPrice(
    productId: string,
    companyId: string,
    quantity: number
  ): Promise<B2BPrice> {
    const company = await this.getCompany(companyId);
    
    // 1. 기업별 계약 단가 조회
    const contractPrice = await this.getContractPrice(productId, companyId);
    if (contractPrice) return contractPrice;
    
    // 2. 등급별 단가
    const tierPrice = await this.getTierPrice(productId, company.tier);
    
    // 3. 수량 구간별 할인
    const volumeDiscount = this.getVolumeDiscount(quantity);
    
    const basePrice = tierPrice ?? await this.getListPrice(productId);
    const finalPrice = basePrice * (1 - volumeDiscount);
    
    return {
      unitPrice: finalPrice,
      totalPrice: finalPrice * quantity,
      discountRate: volumeDiscount,
      priceType: contractPrice ? 'CONTRACT' : 'TIER',
    };
  }
  
  private getVolumeDiscount(quantity: number): number {
    if (quantity >= 1000) return 0.20;
    if (quantity >= 500) return 0.15;
    if (quantity >= 100) return 0.10;
    if (quantity >= 50) return 0.05;
    return 0;
  }
}

2. 견적 요청(RFQ) 시스템

2.1 견적 워크플로우

구매 담당자 (PURCHASER)
    ↓ 견적 요청 작성
견적 요청 (RFQ)
    ↓ 영업 담당자에게 배정
영업 팀 검토
    ↓ 가격/조건 협의
견적서(Quotation) 발행
    ↓ 구매 담당자 확인
견적서 수락
    ↓ 승인 플로우 시작
내부 승인 프로세스
    ↓ 최종 승인
주문 확정 → 납품 → 세금계산서

2.2 견적 데이터 모델

CREATE TABLE rfq_requests (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  company_id      UUID NOT NULL REFERENCES companies(id),
  requester_id    UUID NOT NULL REFERENCES company_users(id),
  status          VARCHAR(20) NOT NULL DEFAULT 'SUBMITTED',
  -- SUBMITTED, IN_REVIEW, QUOTED, ACCEPTED, REJECTED, EXPIRED
  
  delivery_request_date  DATE,
  delivery_address_id    UUID,
  special_requirements   TEXT,
  
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  expires_at      TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 days'
);

CREATE TABLE rfq_items (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  rfq_id      UUID NOT NULL REFERENCES rfq_requests(id),
  product_id  UUID NOT NULL,
  quantity    INTEGER NOT NULL,
  target_unit_price DECIMAL(12, 2),  -- 구매 희망 단가 (선택)
  notes       TEXT
);

CREATE TABLE quotations (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  rfq_id      UUID NOT NULL REFERENCES rfq_requests(id),
  sales_rep_id UUID NOT NULL,
  valid_until  TIMESTAMPTZ NOT NULL,
  
  total_amount     DECIMAL(12, 2) NOT NULL,
  discount_amount  DECIMAL(12, 2) NOT NULL DEFAULT 0,
  payment_terms    VARCHAR(20) NOT NULL,
  delivery_days    INTEGER,
  
  notes       TEXT,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

3. 다단계 구매 승인 워크플로우

3.1 승인 규칙 설계

기업마다 승인 정책이 다르다. 유연한 규칙 엔진이 필요하다.

interface ApprovalRule {
  id: string;
  companyId: string;
  name: string;
  conditions: ApprovalCondition[];
  approvalSteps: ApprovalStep[];
}

interface ApprovalCondition {
  field: 'order_amount' | 'category' | 'requester_role';
  operator: 'gt' | 'lt' | 'eq' | 'in';
  value: number | string | string[];
}

interface ApprovalStep {
  order: number;
  approverType: 'USER' | 'GROUP' | 'ROLE';
  approverIds: string[];
  requireAll: boolean;  // true: 모두 승인, false: 한 명이면 충분
  timeoutHours: number;
  escalationTo?: string;
}

// 예시 규칙: 100만 원 이상 주문은 팀장 → 부서장 2단계 승인
const exampleRule: ApprovalRule = {
  id: 'rule-1',
  companyId: 'company-1',
  name: '대규모 구매 승인',
  conditions: [{ field: 'order_amount', operator: 'gt', value: 1000000 }],
  approvalSteps: [
    { order: 1, approverType: 'ROLE', approverIds: ['TEAM_LEAD'], requireAll: false, timeoutHours: 24 },
    { order: 2, approverType: 'ROLE', approverIds: ['DEPT_MANAGER'], requireAll: false, timeoutHours: 48 },
  ],
};

3.2 승인 처리 서비스

class ApprovalWorkflowService {
  async initiateApproval(orderId: string): Promise<ApprovalInstance> {
    const order = await this.orderRepo.findById(orderId);
    const rules = await this.getRulesForOrder(order);
    
    if (rules.length === 0) {
      // 승인 불필요 → 즉시 진행
      await this.orderRepo.updateStatus(orderId, 'APPROVED');
      return { required: false };
    }
    
    const instance = await this.createApprovalInstance(order, rules[0]);
    await this.notifyApprovers(instance.currentStep);
    
    return instance;
  }
  
  async processDecision(
    instanceId: string,
    approverId: string,
    decision: 'APPROVE' | 'REJECT',
    comment?: string
  ): Promise<void> {
    const instance = await this.getApprovalInstance(instanceId);
    
    await this.recordDecision(instance, approverId, decision, comment);
    
    if (decision === 'REJECT') {
      await this.orderRepo.updateStatus(instance.orderId, 'REJECTED');
      await this.notifyRequester(instance, 'REJECTED', comment);
      return;
    }
    
    const allApproved = await this.checkStepCompletion(instance);
    if (allApproved) {
      const hasNextStep = await this.advanceToNextStep(instance);
      if (!hasNextStep) {
        await this.orderRepo.updateStatus(instance.orderId, 'APPROVED');
        await this.notifyRequester(instance, 'APPROVED');
      }
    }
  }
}

4. 세금계산서 연동

4.1 전자세금계산서 발행 플로우

// 국세청 전자세금계산서 API 연동
class TaxInvoiceService {
  async issueTaxInvoice(order: B2BOrder): Promise<TaxInvoice> {
    const invoice = {
      issueDate: new Date().toISOString().split('T')[0],
      supplyValue: order.supplyAmount,
      taxAmount: order.taxAmount,
      totalAmount: order.totalAmount,
      
      supplier: {
        registrationNumber: process.env.COMPANY_REG_NUMBER,
        name: '판매 회사명',
        ceoName: '대표자',
        address: '회사 주소',
        email: 'tax@seller.com',
      },
      
      recipient: {
        registrationNumber: order.company.businessNumber,
        name: order.company.name,
        email: order.company.taxInvoiceEmail,
      },
      
      items: order.items.map(item => ({
        name: item.productName,
        quantity: item.quantity,
        unitPrice: item.unitPrice,
        supplyValue: item.supplyValue,
        taxAmount: item.taxAmount,
      })),
    };
    
    // 세금계산서 발행 API 호출
    const response = await this.taxApiClient.issue(invoice);
    return response;
  }
}

마무리: B2B 이커머스 구축 체크리스트

  • 기업 계정 계층 구조 (본사 → 부서 → 사용자)
  • B2B 전용 가격 정책 (계약가, 등급가, 수량 할인)
  • 견적 요청(RFQ) → 견적서 발행 플로우
  • 유연한 다단계 승인 규칙 엔진
  • 외상 한도 관리 및 신용 관리
  • 전자세금계산서 자동 발행
  • B2B 전용 고객 포털 (주문 내역, 견적, 청구서)

B2B 이커머스는 복잡하지만, 잘 구현하면 고객 락인 효과와 높은 LTV를 만든다.