Skip to main content

B2B 개발

Shopify에서의 B2B(기업간) 커머스는 빠르게 성장하는 분야이며, Shopify Plus는 앱 개발자가 확장하고 연동할 수 있는 네이티브 B2B 기능을 제공합니다. 이 모듈에서는 B2B 데이터 모델, 기업 계정 및 위치, 카탈로그를 통한 커스텀 가격 책정, 구매 주문 승인 워크플로우, 결제 커스터마이제이션, 결제 수단 연동을 다룹니다.

Shopify Plus의 B2B 기능

Shopify의 네이티브 B2B 기능은 Shopify Plus 플랜에서만 사용할 수 있습니다. 앱 개발자로서 이러한 기능을 이해하면 판매자가 복잡한 도매 운영에 필요로 하는 보완 도구를 구축할 수 있습니다.

B2B 데이터 모델 개요

Company
├── Company Locations (shipping/billing addresses)
│ ├── Catalog Assignments (custom pricing)
│ └── Payment Terms (Net 30, Net 60, etc.)
├── Company Contacts (buyer accounts)
│ └── Roles & Permissions
└── Orders (linked to company + location)
info

B2B와 DTC(소비자 직접 판매)는 동일한 Shopify 스토어에서 운영됩니다. 하나의 스토어가 소매 고객과 도매 구매자 모두를 서비스할 수 있습니다. 로그인한 고객이 B2B 기업과 연결되어 있는지 여부에 따라 스토어프론트 경험이 달라집니다.

B2B 기업 조회

query GetCompanies($first: Int!, $query: String) {
companies(first: $first, query: $query) {
edges {
node {
id
name
externalId
note
customerSince
orderCount
totalSpent {
amount
currencyCode
}
mainContact {
customer {
firstName
lastName
email
}
}
locations(first: 10) {
edges {
node {
id
name
shippingAddress {
address1
city
province
country
zip
}
catalog {
id
title
}
buyerExperienceConfiguration {
paymentTermsTemplate {
paymentTermsType
dueInDays
}
}
}
}
}
}
}
}
}

기업 계정 및 위치

프로그래밍 방식으로 기업 생성

ERP 시스템이나 다른 플랫폼에서 도매 고객을 가져오는 앱은 API를 통해 기업을 생성해야 합니다:

// services/b2b-company-service.js
export async function createCompany(client, companyData) {
const response = await client.query({
data: {
query: `mutation CompanyCreate($input: CompanyCreateInput!) {
companyCreate(input: $input) {
company {
id
name
}
userErrors {
field
message
}
}
}`,
variables: {
input: {
company: {
name: companyData.name,
externalId: companyData.erpId,
note: companyData.notes,
},
companyLocation: {
name: companyData.headquarters.name || 'Headquarters',
shippingAddress: {
address1: companyData.headquarters.address1,
address2: companyData.headquarters.address2,
city: companyData.headquarters.city,
provinceCode: companyData.headquarters.state,
countryCode: companyData.headquarters.country,
zip: companyData.headquarters.zip,
},
buyerExperienceConfiguration: {
paymentTermsTemplateId: companyData.paymentTermsId,
},
},
companyContact: {
customer: {
firstName: companyData.primaryContact.firstName,
lastName: companyData.primaryContact.lastName,
email: companyData.primaryContact.email,
},
},
},
},
},
});

const result = response.body.data.companyCreate;

if (result.userErrors.length > 0) {
throw new Error(
`Failed to create company: ${result.userErrors.map((e) => e.message).join(', ')}`
);
}

return result.company;
}

다중 위치 관리

대규모 B2B 고객은 각각 다른 가격 책정과 결제 조건을 가진 여러 배송 위치를 보유하는 경우가 많습니다:

export async function addCompanyLocation(client, companyId, locationData) {
const response = await client.query({
data: {
query: `mutation CompanyLocationCreate(
$companyId: ID!,
$input: CompanyLocationInput!
) {
companyLocationCreate(companyId: $companyId, input: $input) {
companyLocation {
id
name
}
userErrors {
field
message
}
}
}`,
variables: {
companyId,
input: {
name: locationData.name,
shippingAddress: locationData.address,
buyerExperienceConfiguration: {
paymentTermsTemplateId: locationData.paymentTermsId,
checkoutToDraft: locationData.requiresApproval,
},
},
},
},
});

return response.body.data.companyLocationCreate;
}
tip

주문에 관리자 승인이 필요한 기업 위치에 checkoutToDraft: true를 설정하십시오. 이렇게 하면 해당 위치의 주문이 확정 주문 대신 초안 주문으로 생성되어 승인 워크플로우를 활성화할 수 있습니다.

커스텀 가격 책정과 카탈로그

카탈로그는 Shopify가 B2B 전용 가격 책정을 관리하는 방법입니다. 각 카탈로그에는 지정된 기업 위치에 대해 스토어 기본 가격을 덮어쓰는 가격 목록이 포함되어 있습니다.

커스텀 가격으로 카탈로그 생성

// services/catalog-service.js
export async function createCatalogWithPricing(client, catalogData) {
// Step 1: Create the catalog
const catalogResponse = await client.query({
data: {
query: `mutation CatalogCreate($input: CatalogCreateInput!) {
catalogCreate(input: $input) {
catalog {
id
title
priceList {
id
}
}
userErrors { field message }
}
}`,
variables: {
input: {
title: catalogData.name,
status: 'ACTIVE',
context: {
companyLocationIds: catalogData.locationIds,
},
},
},
},
});

const catalog = catalogResponse.body.data.catalogCreate.catalog;

// Step 2: Add fixed prices to the price list
if (catalogData.fixedPrices?.length > 0) {
await client.query({
data: {
query: `mutation PriceListFixedPricesAdd(
$priceListId: ID!,
$prices: [PriceListPriceInput!]!
) {
priceListFixedPricesAdd(
priceListId: $priceListId,
prices: $prices
) {
prices {
variant { id }
price { amount currencyCode }
}
userErrors { field message }
}
}`,
variables: {
priceListId: catalog.priceList.id,
prices: catalogData.fixedPrices.map((p) => ({
variantId: p.variantId,
price: {
amount: p.price.toString(),
currencyCode: p.currency,
},
compareAtPrice: p.compareAtPrice
? { amount: p.compareAtPrice.toString(), currencyCode: p.currency }
: null,
})),
},
},
});
}

return catalog;
}

비율 기반 가격 조정

더 간단한 도매 할인 구조의 경우 고정 가격 대신 퍼센트 조정을 사용하십시오:

export async function setPercentageAdjustment(client, priceListId, percentage) {
const response = await client.query({
data: {
query: `mutation PriceListUpdate($id: ID!, $input: PriceListInput!) {
priceListUpdate(id: $id, input: $input) {
priceList {
id
adjustment {
type
value
}
}
userErrors { field message }
}
}`,
variables: {
id: priceListId,
input: {
adjustment: {
type: 'PERCENTAGE_DECREASE',
value: percentage, // e.g., 20 for 20% off
},
},
},
},
});

return response.body.data.priceListUpdate;
}
warning

카탈로그 가격은 자동 할인 및 가격 규칙을 포함한 다른 모든 가격 책정 메커니즘보다 우선합니다. B2B 고객의 가격 문제를 디버깅할 때는 항상 카탈로그 할당을 먼저 확인하십시오.

구매 주문 승인 워크플로우 (Winter '26)

Winter '26 기능

Winter '26 릴리스는 B2B에서 구매 주문 승인 워크플로우에 대한 네이티브 지원을 도입합니다. 기업 위치에 지정된 승인자가 주문을 확정하기 전에 검토하도록 요구하는 승인 규칙을 설정할 수 있습니다. 이는 이전의 초안 주문 우회 방법을 대체합니다.

승인 규칙 구성

// services/approval-workflow.js
export async function configureApprovalWorkflow(client, locationId, rules) {
const response = await client.query({
data: {
query: `mutation CompanyLocationApprovalConfig(
$locationId: ID!,
$rules: [ApprovalRuleInput!]!
) {
companyLocationUpdateApprovalRules(
companyLocationId: $locationId,
rules: $rules
) {
companyLocation {
id
approvalRules {
id
condition
threshold { amount currencyCode }
approvers { customer { email } }
}
}
userErrors { field message }
}
}`,
variables: {
locationId,
rules: rules.map((rule) => ({
condition: rule.condition, // 'ORDER_TOTAL_EXCEEDS'
threshold: {
amount: rule.threshold.toString(),
currencyCode: rule.currency,
},
approverContactIds: rule.approverIds,
})),
},
},
});

return response.body.data.companyLocationUpdateApprovalRules;
}

승인 대시보드 구축

// routes/app.approvals.jsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import {
Page,
Layout,
Card,
DataTable,
Badge,
Button,
ButtonGroup,
} from '@shopify/polaris';

export async function loader({ request }) {
const { admin } = await authenticate.admin(request);

const response = await admin.graphql(`{
draftOrders(first: 50, query: "status:open AND tag:b2b-pending-approval") {
edges {
node {
id
name
createdAt
totalPriceSet {
shopMoney { amount currencyCode }
}
purchasingEntity {
... on PurchasingCompany {
company { name }
location { name }
contact { customer { email } }
}
}
}
}
}
}`);

const { data } = await response.json();
return json({ pendingOrders: data.draftOrders.edges });
}

export default function ApprovalsPage() {
const { pendingOrders } = useLoaderData();

const rows = pendingOrders.map(({ node }) => [
node.name,
node.purchasingEntity?.company?.name || 'N/A',
node.purchasingEntity?.location?.name || 'N/A',
`$${node.totalPriceSet.shopMoney.amount}`,
new Date(node.createdAt).toLocaleDateString(),
<ButtonGroup key={node.id}>
<Button tone="success" onClick={() => approveOrder(node.id)}>
Approve
</Button>
<Button tone="critical" onClick={() => rejectOrder(node.id)}>
Reject
</Button>
</ButtonGroup>,
]);

return (
<Page title="Purchase Order Approvals">
<Layout>
<Layout.Section>
<Card>
<DataTable
columnContentTypes={[
'text', 'text', 'text', 'numeric', 'text', 'text',
]}
headings={[
'Order', 'Company', 'Location', 'Total', 'Date', 'Actions',
]}
rows={rows}
/>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}

B2B 결제 커스터마이제이션

Shopify Plus 판매자는 Checkout UI Extensions를 사용하여 B2B 결제 경험을 커스터마이즈할 수 있습니다.

PO 번호 필드 추가

// extensions/checkout-b2b/src/Checkout.jsx
import {
reactExtension,
TextField,
useApplyMetafieldsChange,
useMetafield,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
'purchase.checkout.shipping-option-list.render-after',
() => <PONumberField />
);

function PONumberField() {
const poNumber = useMetafield({
namespace: 'custom',
key: 'po_number',
});

const applyMetafieldsChange = useApplyMetafieldsChange();

const handleChange = (value) => {
applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'custom',
key: 'po_number',
valueType: 'string',
value,
});
};

return (
<TextField
label="Purchase Order Number"
value={poNumber?.value || ''}
onChange={handleChange}
/>
);
}

최소 주문 수량 적용

// extensions/checkout-b2b/src/MinOrderValidation.jsx
import {
reactExtension,
useBuyerJourneyIntercept,
useCartLines,
Banner,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
'purchase.checkout.block.render',
() => <MinOrderValidation />
);

function MinOrderValidation() {
const cartLines = useCartLines();

const totalQuantity = cartLines.reduce(
(sum, line) => sum + line.quantity,
0
);

const minimumOrder = 24; // Configurable per company

useBuyerJourneyIntercept(({ canBlockProgress }) => {
if (canBlockProgress && totalQuantity < minimumOrder) {
return {
behavior: 'block',
reason: `Minimum order quantity is ${minimumOrder} units`,
errors: [
{
message: `Your cart has ${totalQuantity} items. The minimum order is ${minimumOrder} items.`,
},
],
};
}

return { behavior: 'allow' };
});

if (totalQuantity < minimumOrder) {
return (
<Banner status="warning">
Minimum order is {minimumOrder} units. You currently have{' '}
{totalQuantity}. Please add {minimumOrder - totalQuantity} more items.
</Banner>
);
}

return null;
}

ACH 결제 연동

많은 B2B 거래는 신용카드 대신 ACH(자동 청산소) 은행 이체를 사용합니다. Shopify는 결제 앱과 수동 결제 수단을 통해 ACH를 지원합니다.

결제 조건을 통한 ACH 설정

// services/ach-payment-service.js
export async function configureACHPaymentTerms(client, locationId) {
// First, find or create the ACH payment terms template
const response = await client.query({
data: {
query: `query PaymentTermsTemplates {
paymentTermsTemplates {
id
name
paymentTermsType
dueInDays
}
}`,
},
});

const templates = response.body.data.paymentTermsTemplates;
const net30 = templates.find(
(t) => t.paymentTermsType === 'NET' && t.dueInDays === 30
);

// Assign payment terms to the company location
await client.query({
data: {
query: `mutation AssignPaymentTerms(
$locationId: ID!,
$paymentTermsTemplateId: ID!
) {
companyLocationAssignPaymentTerms(
companyLocationId: $locationId,
paymentTermsTemplateId: $paymentTermsTemplateId
) {
companyLocation {
buyerExperienceConfiguration {
paymentTermsTemplate {
paymentTermsType
dueInDays
}
}
}
userErrors { field message }
}
}`,
variables: {
locationId,
paymentTermsTemplateId: net30.id,
},
},
});
}
danger

B2B 결제 조건(Net 30, Net 60 등)은 판매자에게 실질적인 재정적 위험을 초래합니다. 앱이 결제 조건 할당을 자동화하는 경우, 새로운 기업에 조건을 연장하기 전에 항상 신용 한도 확인, 결제 이력 검증, 판매자 승인 흐름 등의 안전장치를 포함하십시오.

B2B 개발 체크리스트

기능복잡도Shopify Plus 필요
기업 생성 및 관리중간
멀티 위치 지원중간
카탈로그 기반 가격 책정높음
구매 주문 승인높음예 (Winter '26)
결제 커스터마이제이션높음
결제 조건 / ACH중간
B2B 고객 포털높음

Shopify에서의 B2B 커머스는 아직 성숙 단계에 있으며, 앱 개발자에게는 상당한 기회가 있습니다. 레거시 도매 플랫폼에서 마이그레이션하는 판매자들은 Shopify의 네이티브 기능과 복잡한 운영 요구 사항 간의 격차를 해소하는 앱이 필요합니다. 지금 이 분야에서 전문성을 구축하면 Shopify가 B2B 기능에 지속적으로 대규모 투자하는 가운데 좋은 위치를 확보할 수 있습니다.