WooGraphQL과 헤드리스 숍 구축: 제 2 장 5

banner

맨 위: 이 튜토리얼은 이전 WooGraphQL 릴리스를 다루고 더 이상 날짜가 없습니다. rewritten version은 진행 중입니다. 이 버전은 여기에서 공개됩니다.

https://www.youtube.com/embed/nhlexP-KtSo

본문 바로가기

  1. Headless eCommerce 응용 프로그램을위한 WordPress 설정
  2. eCommerce 앱 만들기 및 설정
  3. 빌딩 숍 목록 페이지 WooGraphQL
  4. 사용자 로그인 및 탐색
  5. 제품 페이지 및 카트 옵션 만들기

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-request

2. 새 만들기 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

Your email address will not be published. Required fields are marked *