맨 위: 이 튜토리얼은 이전 WooGraphQL 릴리스를 다루고 더 이상 날짜가 없습니다. rewritten version은 진행 중입니다. 이 버전은 여기에서 공개됩니다.
본문 바로가기
- Headless eCommerce 응용 프로그램을위한 WordPress 설정
- eCommerce 앱 만들기 및 설정
- 빌딩 숍 목록 페이지 WooGraphQL
- 사용자 로그인 및 탐색
- 제품 페이지 및 카트 옵션 만들기
headless eCommerce 응용 프로그램을 만드는 튜토리얼 시리즈의 두 번째 부분에 오신 것을 환영합니다! 이 다음 챕터는 e-commerce 앱을 설치하여 상점과 인터페이스합니다. 프론트엔드를 위한 Next.js를 활용하고 다른 여러 도구를 활용할 수 있습니다. shadcn/이 구성 요소 및 GraphQL 코드 생성기 GraphQL 작업 처리.
e-commerce 앱 만들기 및 설정
WooGraphQL을 사용하여 WooCommerce 저장소를 설정 한 후 GraphQL 엔드포인트로 왼쪽해야합니다. https://site-name/graphql, 이 마지막 장에서 번들 개발 설정을 사용하는 경우 http://localhost:8080/wp/graphql. 그 말로, 시작하자.
Next.js 응용 프로그램 만들기
Next.js 응용 프로그램을 만들고 필요한 구성을 설정하여 시작합시다.
1. 첫째로, 당신의 맨끝에 있는 뒤에 오는 명령을 실행해서 새로운 Next.js 신청을 생성합니다: npx create-next-app@latest 새로 생성된 프로젝트 디렉토리로 이동합니다.
2. 그때, 설치 dotenv-flow · deepmerge 터미널에서 다음 명령을 실행하여 npm 패키지: npm i dotenv-flow deepmerge
3. 새 만들기 .env.local 프로젝트의 루트 디렉토리에 파일을 작성하고 다음 세부 사항을 작성하십시오.
GRAPHQL_ENDPOINT=[GRAPHQL_ENDPOINT]
FRONTEND_URL=[NEXT_APP_URL]
BACKEND_URL=[WP_URL]
SITE_NAME=[SITE_NAME]
SITE_DESCRIPTION=[SITE_DESCRIPTION]괄목된 값을 각각의 동등물로 교체하는 것을 잊지 마세요.
4. 새 만들기 next.config.js 프로젝트의 루트 디렉토리에 파일 및 다음 코드를 추가:
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
dangerouslyAllowSVG: true,
formats: ['image/avif', 'image/webp'],
domains: ['localhost'],
minimumCacheTTL: 60,
disableStaticImages: true,
},
env: {
GRAPHQL_ENDPOINT: process.env.GRAPHQL_ENDPOINT,
FRONTEND_URL: process.env.FRONTEND_URL,
BACKEND_URL: process.env.BACKEND_URL,
SITE_NAME: process.env.SITE_NAME,
SITE_DESCRIPTION: process.env.SITE_DESCRIPTION,
},
}
module.exports = nextConfig;5. 업데이트 content 내 계정 tailwind.config.js 다음 파일:
...
content: [
'./ui/**/*.{ts,tsx}',
'./server/**/*.{ts,tsx}',
'./client/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
]
...설치하기 shadcn/ui 제품정보
우리는 사용할 것입니다 shadcn/ui 응용 프로그램의 UI 구성 요소에 대한 라이브러리. 설치하고 구성하는 방법은 다음과 같습니다.
1. shadcn-ui init 명령을 실행하십시오: npx shadcn-ui@latest init 그런 다음 프롬프트를 따르십시오.
2. 열기 components.json 생성 된 파일 및 다음과 같은 내용을 대체 :
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "/",
"utils": "@/utils/ui"
}
}3. 이름 lib/utils.ts 파일 형식 /utils/ui.ts 그리고 만들기 /ui 디렉토리.
4. 당신의 맨끝에 있는 뒤에 오는 명령을 실행해서 당신의 프로젝트에 필요한 성분을 추가하십시오:
npx shadcn-ui@latest add aspect-ratio
npx shadcn-ui@latest add badge
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add form
npx shadcn-ui@latest add input
npx shadcn-ui@latest add label
npx shadcn-ui@latest add radio-group
npx shadcn-ui@latest add select
npx shadcn-ui@latest add sheet
npx shadcn-ui@latest add slider
npx shadcn-ui@latest add tabs
npx shadcn-ui@latest add toast다른 UI 구성품 만들기
우리는 우리의 신청을 위한 몇몇 추가 성분을 창조할 것입니다.
1. 새 만들기 ui/Image/index.ts 파일과 다음 코드를 추가:
'use client';
export * from './Image';
2. 새 만들기 ui/Image/Image.tsx 파일과 다음 코드를 추가:
import { useState } from 'react';
import NextImage from 'next/image';
import { cn } from '@/utils/ui';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { AspectRatio } from "@/ui/aspect-ratio"
export type ImageProps = {
className?: string;
src: string;
sizes?: string;
width?: number;
height?: number;
ratio?: number;
alt: string;
style?: JSX.IntrinsicElements['img']['style']
fill?: boolean;
priority?: boolean;
}
export function Image(props: ImageProps) {
const [isLoading, setLoading] = useState(true);
const {
className = '',
src,
alt,
sizes,
width,
height,
ratio,
style,
fill = true,
priority,
} = props;
return (
<div
className={cn(
'overflow-hidden group relative',
className && className,
)}
style={style}
>
<AspectRatio ratio={ratio}>
{isLoading && (
<LoadingSpinner className="position absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"/>
)}
<NextImage
src={src}
alt={alt}
width={width as number}
height={height as number}
sizes={sizes}
className={cn(
'group-hover:opacity-75 object-cover',
'duration-700 ease-in-out',
isLoading
? 'grayscale blur-2xl scale-110'
: 'grayscale-0 blur-0 scale-100',
)}
fill={fill}
onLoadingComplete={() => setLoading(false)}
priority={priority}
/>
</AspectRatio>
</div>
);
}
3. 새 만들기 ui/LoadingSpinner/index.ts 파일과 다음 코드를 추가:
export * from './LoadingSpinner';
4. 새 만들기 ui/LoadingSpinner/LoadingSpinner.tsx 파일과 다음 코드를 추가:
export interface LoadingSpinnerProps {
className?: string;
noText?: boolean;
color?: string;
}
export function LoadingSpinner({ className = '', noText = false, color = 'amber-400' }: LoadingSpinnerProps) {
return (
<div aria-label="Loading..." role="status" className={`relative flex items-center justify-center space-x-2 ${className}`}>
<svg className="h-6 w-6 animate-spin stroke-amber-600" viewBox="0 0 256 256">
<line x1="128" y1="32" x2="128" y2="64" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
<line
x1="195.9"
y1="60.1"
x2="173.3"
y2="82.7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="24"
/>
<line x1="224" y1="128" x2="192" y2="128" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
<line
x1="195.9"
y1="195.9"
x2="173.3"
y2="173.3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="24"
/>
<line x1="128" y1="224" x2="128" y2="192" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
<line
x1="60.1"
y1="195.9"
x2="82.7"
y2="173.3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="24"
/>
<line x1="32" y1="128" x2="64" y2="128" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
<line
x1="60.1"
y1="60.1"
x2="82.7"
y2="82.7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="24"
/>
</svg>
{!noText && <span className="text-xs font-medium text-gray-500">Loading...</span>}
</div>
);
}
5. 새 만들기 ui/NavLink/index.ts 파일과 다음 코드를 추가:
export * from './NavLink';
6. 새 만들기 ui/NavLink/NavLink.tsx 파일과 다음 코드를 추가:
import { PropsWithChildren } from 'react';
import Link from 'next/link';
import { cn } from '@woographql/utils/ui';
export const linkClassName = 'transition-colors group-hover:text-blue-400';
export interface NavLinkProps {
href: string;
className?: string;
shallow?: boolean;
prefetch?: boolean;
}
export function NavLink(props: PropsWithChildren<NavLinkProps>) {
const {
children,
href,
className,
shallow,
prefetch,
} = props;
return (
<Link
className={cn(
className,
linkClassName,
)}
href={href}
shallow={shallow}
prefetch={prefetch}
>
{children}
</Link>
);
}
GraphQL Code Generator 설치 및 구성
마지막으로, 우리는 GraphQL 코드 생성기를 설정해야합니다 (코드젠) Typed GraphQL 작업 생성을 위해. Codegen은 실제 시간 평균이며 구성 요소 또는 큰 GraphQL 스키마를 많이 사용하는 경우해야합니다. GraphQL 엔드포인트와 관련된 코드를 생성하는 방법을 기반으로 합니다. 애플리케이션에서 GraphQL 엔드포인트의 TypeScript 유형을 생성하는 데 사용됩니다. GraphQL 작업에 대한 TS 유형은 작성되며, 실행이 실행되는 neat 래퍼는 바람을 사용하여 작동합니다. graphql-request 도서관.
1. GraphQL 코드 생성기를 터미널에서 다음 명령을 실행하여 필요한 플러그인과 함께 설치하십시오.
npm i -D npm-run-all @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request2. 새 만들기 codegen.ts 프로젝트의 루트 디렉토리에 파일 및 다음 코드를 추가:
import type { CodegenConfig } from '@graphql-codegen/cli';
import dotenv from 'dotenv-flow';
dotenv.config({ silent: true });
const config: CodegenConfig = {
schema: process.env.GRAPHQL_ENDPOINT,
documents: ['graphql/**/*.graphql'],
verbose: true,
overwrite: true,
generates: {
'graphql/generated.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-graphql-request',
],
config: {
namingConvention: 'keep',
}
}
}
}
export default config;
3. 업데이트 scripts 내 계정 package.json GraphQL 운영 및 래퍼를 생성하기위한 스크립트를 포함 할 파일 :
{
...
"scripts": {
"next:dev": "next dev",
"next:build": "next build",
"start": "next start",
"lint": "next lint",
"codegen:dev": "graphql-codegen --config codegen.ts --watch",
"codegen:build": "graphql-codegen --config codegen.ts",
"dev": "run-p codegen:dev next:dev",
"build": "NODE_ENV=production run-s codegen:build next:build",
},
...
}
4. 새 만들기 graphql/index.ts 파일과 다음 코드를 추가:
export * from './generated';
export * from './client';
5. 새 만들기 graphql/main.graphql 파일과 다음 코드를 추가:
fragment MenuItemContent on MenuItem {
id
uri
title
label
cssClasses
}
fragment MenuItemRecursive on MenuItem {
...MenuItemContent
childItems {
nodes {
...MenuItemContent
}
}
}
fragment MenuContent on Menu {
id
name
locations
slug
menuItems(first: 20, where: {parentId: 0}) {
nodes {
...MenuItemRecursive
}
}
}
fragment CustomerContent on Customer {
id
sessionToken
firstName
shipping {
postcode
state
city
country
}
}
fragment ProductContentSlice on Product {
id
databaseId
name
slug
type
image {
id
sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
altText
}
... on SimpleProduct {
price
regularPrice
soldIndividually
}
... on VariableProduct {
price
regularPrice
soldIndividually
}
}
fragment ProductVariationContentSlice on ProductVariation {
id
databaseId
name
slug
image {
id
sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
altText
}
price
regularPrice
}
fragment ProductContentSmall on Product {
id
databaseId
slug
name
type
shortDescription(format: RAW)
image {
id
sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
altText
}
productCategories(first: 20) {
nodes {
id
slug
name
}
}
productTags(first: 20) {
nodes {
id
slug
name
}
}
allPaColor(first: 100) {
nodes {
id
slug
name
}
}
... on SimpleProduct {
onSale
stockStatus
price
rawPrice: price(format: RAW)
regularPrice
salePrice
soldIndividually
}
... on VariableProduct {
onSale
stockStatus
price
rawPrice: price(format: RAW)
regularPrice
salePrice
soldIndividually
}
}
fragment ProductContentFull on Product {
id
databaseId
slug
name
type
description
shortDescription(format: RAW)
image {
id
sourceUrl
altText
}
galleryImages {
nodes {
id
sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
altText
}
}
productTags(first: 20) {
nodes {
id
slug
name
}
}
attributes {
nodes {
id
attributeId
... on LocalProductAttribute {
name
label
options
variation
}
... on GlobalProductAttribute {
name
label
options
variation
slug
terms(first: 100) {
nodes {
id
name
slug
}
}
}
}
}
... on SimpleProduct {
onSale
stockStatus
price
rawPrice: price(format: RAW)
regularPrice
salePrice
stockStatus
stockQuantity
soldIndividually
defaultAttributes(first: 100) {
nodes {
id
attributeId
name
value
label
}
}
}
... on VariableProduct {
onSale
price
rawPrice: price(format: RAW)
regularPrice
salePrice
stockStatus
stockQuantity
soldIndividually
defaultAttributes(first: 100) {
nodes {
id
attributeId
label
name
value
}
}
variations(first: 50) {
nodes {
id
databaseId
name
price
rawPrice: price(format: RAW)
regularPrice
salePrice
onSale
attributes {
nodes {
name
label
value
}
}
image {
id
sourceUrl
altText
}
}
}
}
}
fragment VariationContent on ProductVariation {
id
name
slug
price
regularPrice
salePrice
stockStatus
stockQuantity
onSale
image {
id
sourceUrl
altText
}
}
fragment CartItemContent on CartItem {
key
product {
node {
...ProductContentSlice
}
}
variation {
attributes {
id
label
name
value
}
node {
...ProductVariationContentSlice
}
}
quantity
total
subtotal
subtotalTax
extraData {
key
value
}
}
fragment CartContent on Cart {
contents(first: 100) {
itemCount
nodes {
...CartItemContent
}
}
appliedCoupons {
code
discountAmount
discountTax
}
needsShippingAddress
availableShippingMethods {
packageDetails
supportsShippingCalculator
rates {
id
instanceId
methodId
label
cost
}
}
subtotal
subtotalTax
shippingTax
shippingTotal
total
totalTax
feeTax
feeTotal
discountTax
discountTotal
}
fragment CustomerFields on Customer {
id
databaseId
firstName
lastName
displayName
email
sessionToken
metaData {
key
value
}
}
query GetTopNav {
menu(id: "primary", idType: LOCATION) {
...MenuContent
}
}
query GetProducts($first: Int, $after: String, $where: RootQueryToProductConnectionWhereArgs) {
products(first: $first, after: $after, where: $where) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
...ProductContentSmall
}
}
nodes {
...ProductContentSmall
}
}
}
query GetProduct($id: ID!, $idType: ProductIdTypeEnum) {
product(id: $id, idType: $idType) {
...ProductContentFull
}
}
query GetShopCategories($first: Int, $after: String, $where: RootQueryToProductCategoryConnectionWhereArgs) {
productCategories(first: $first, after: $after, where: $where) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
slug
}
}
nodes {
id
name
slug
}
}
}
query GetShopTags($first: Int, $after: String, $where: RootQueryToProductTagConnectionWhereArgs) {
productTags(first: $first, after: $after, where: $where) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
slug
}
}
nodes {
id
name
slug
}
}
}
query GetShopColors($first: Int, $after: String, $where: RootQueryToPaColorConnectionWhereArgs) {
allPaColor(first: $first, after: $after, where: $where) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
slug
}
}
nodes {
id
name
slug
}
}
}
query GetSession {
cart {
...CartContent
}
customer {
...CustomerContent
}
}
mutation AddToCart(
$productId: Int!,
$variationId: Int,
$quantity: Int,
$variation: [ProductAttributeInput],
$extraData: String
) {
addToCart(
input: {
productId: $productId,
variationId: $variationId,
quantity: $quantity,
variation: $variation,
extraData: $extraData
}
) {
cart {
...CartContent
}
cartItem {
...CartItemContent
}
}
}
mutation UpdateCartItemQuantities($items: [CartItemQuantityInput]) {
updateItemQuantities(input: {items: $items}) {
cart {
...CartContent
}
items {
...CartItemContent
}
}
}
mutation RemoveItemsFromCart($keys: [ID], $all: Boolean) {
removeItemsFromCart(input: {keys: $keys, all: $all}) {
cart {
...CartContent
}
cartItems {
...CartItemContent
}
}
}
mutation Login($username: String!, $password: String!) {
login(input: { username: $username, password: $password }) {
authToken
refreshToken
customer {
...CustomerFields
}
}
}
mutation RefreshAuthToken($refreshToken: String!) {
refreshJwtAuthToken(input: { jwtRefreshToken: $refreshToken }) {
authToken
}
}
mutation UpdateSession($input: UpdateSessionInput!) {
updateSession(input: $input) {
session {
id
key
value
}
}
}6. 새 만들기 graphql/client.ts 파일과 다음 코드를 추가:
import { GraphQLClient } from 'graphql-request';
import deepmerge from 'deepmerge';
import {
RootQueryToProductConnectionWhereArgs,
Product,
ProductCategory,
PaColor,
getSdk,
RootQueryToProductCategoryConnectionWhereArgs,
RootQueryToPaColorConnectionWhereArgs,
ProductIdTypeEnum,
} from './generated';
let client: GraphQLClient;
export function getClient() {
const endpoint = process.env.GRAPHQL_ENDPOINT
if (!endpoint) {
throw new Error('GRAPHQL_ENDPOINT is not defined')
}
if (!client) {
client = new GraphQLClient(endpoint);
}
return client;
}
export function getClientWithSdk() {
return getSdk(getClient());
}
const initialConnectionResults = {
pageInfo: {
hasNextPage: true,
endCursor: null,
},
edges: [],
nodes: [],
};
export async function fetchProducts(
pageSize: number,
pageLimit = 0,
where?: RootQueryToProductConnectionWhereArgs
) {
try {
const client = getClientWithSdk();
let data = { products: initialConnectionResults }
let after = '';
let count = 0
while(data.products.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
const next = await client.GetProducts({
first: pageSize,
after,
where,
});
data = deepmerge(data, next);
after = next.products?.pageInfo.endCursor || '';
count++;
}
return (data.products.nodes) as Product[];
} catch (err) {
console.error(err || 'Failed to fetch product listing!!!');
}
}
export async function fetchCategories(
pageSize: number,
pageLimit = 0,
where?: RootQueryToProductCategoryConnectionWhereArgs
) {
try {
const client = getClientWithSdk();
let data = { productCategories: initialConnectionResults }
let after = '';
let count = 0
while(data.productCategories.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
const next = await client.GetShopCategories({
first: pageSize,
after,
where,
});
data = deepmerge(data, next);
after = next.productCategories?.pageInfo.endCursor || '';
count++;
}
return (data.productCategories.nodes) as ProductCategory[];
} catch (err) {
console.error(err || 'Failed to fetch product categories!!!');
}
}
export async function fetchColors(
pageSize: number,
pageLimit = 0,
where?: RootQueryToPaColorConnectionWhereArgs
) {
try {
const client = getClientWithSdk();
let data = { allPaColor: initialConnectionResults }
let after = '';
let count = 0
while(data.allPaColor.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
const next = await client.GetShopColors({
first: pageSize,
after,
where
});
data = deepmerge(data, next);
after = next.allPaColor?.pageInfo.endCursor || '';
count++;
}
return (data.allPaColor.nodes) as PaColor[];
} catch (err) {
console.error(err || 'Failed to fetch product color attributes!!!');
}
}
export async function fetchProductBy(slug: string, idType: ProductIdTypeEnum) {
try {
const client = getClientWithSdk();
const data = await client.GetProduct({
id: slug,
idType: idType,
});
if (!data.product) {
throw new Error('Product not found!!!');
}
return data.product as Product;
} catch (err) {
console.error(err || 'Failed to fetch product data!!!');
}
}
관련 기사
감사합니다! GraphQL 엔드포인트를 사용하여 전자 상거래 개발을 위한 Next.js 애플리케이션을 성공적으로 설정했습니다. 설치 및 구성 shadcn/ui 구성 요소, 다른 UI 구성 요소를 생성하고 GraphQL 코드 생성기를 구성. 응용 프로그램은 이제 GraphQL을 사용하여 더 개발 및 통합 할 준비가되었습니다.

Leave a Reply