이 글은 누구를 위한 것인가
- 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를 만든다.