01.software 문서

상품 상세 페이지

Async route params로 상품 상세와 product selection을 표시합니다.

상품 상세 페이지

준비

상품 listing과 같은 key·market·content locale context를 사용하세요. 이 완결 페이지는 Next.js 16 Promise params, searchParams를 await하며 미정의 ProductView에 의존하지 않습니다. listing 레시피의 읽기 오류 boundary를 app/products/error.tsx에 추가하세요.

예제는 공개 SDK 0.65.1, Next.js 16.3.8 App Router, React 19.2.4, Node 24.15.0 조합입니다. 자신의 앱에 npm install @01.software/sdk@0.65.1로 설치하세요. force-dynamic으로 SDK 호출을 build가 아닌 요청 때 실행합니다. cacheComponents와 static export를 쓰지 않는 구성입니다. 컴파일과 통제된 로컬 transport 검사는 hosted 제공 여부나 원격 tenant admission의 증거가 아닙니다.

상품 상세와 선택 표시

// app/products/[slug]/page.tsx
import {
  createClient,
  createProductSelectionCodec,
  resolveProductSelection,
  ProductSelectionCodecError,
} from '@01.software/sdk'
import { notFound } from 'next/navigation'

export const dynamic = 'force-dynamic'

export default async function ProductPage({
  params,
  searchParams,
}: {
  params: Promise<{ slug: string }>
  searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
  const { slug } = await params
  const query = new URLSearchParams()
  for (const [key, value] of Object.entries(await searchParams)) {
    if (typeof value === 'string') query.set(key, value)
  }
  const client = createClient({
    publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
    apiUrl: process.env.SOFTWARE_API_URL,
  })
  const result = await client.commerce.product.detailCatalog({ slug })
  if (!result.found) return notFound()
  const detail = result.product
  const codec = createProductSelectionCodec(detail, { emit: 'canonical-id' })
  let normalized: ReturnType<typeof codec.parse>
  try {
    normalized = codec.parse(query.toString())
  } catch (error) {
    if (!(error instanceof ProductSelectionCodecError)) throw error
    // Stale/ambiguous URL input is not a catalog transport failure.
    normalized = codec.parse('')
  }
  const selection = resolveProductSelection(detail, normalized)
  const selectionQuery = codec.stringify(normalized)
  const href = `/products/${encodeURIComponent(slug)}${selectionQuery ? `?${selectionQuery}` : ''}`
  return (
    <main>
      <h1>
        {typeof detail.product.title === 'string'
          ? detail.product.title
          : (detail.product.title.en ?? detail.product.title.ko ?? 'Untitled')}
      </h1>
      <a href={href}>Selection link</a>
      <p>
        {selection.selectedVariant ? 'Variant selected' : 'Choose an option'}
      </p>
    </main>
  )
}

정상·빈 결과·실패

상품이 있으면 제목과 정규화된 product selection 링크를 표시합니다. listing swatch URL을 따라가면 detail codec이 query를 읽고 variant 또는 부분 선택을 해석합니다. codec.stringify()는 ? 없는 query를 반환하므로 예제는 필요할 때만 구분자를 넣습니다. 없거나 비공개인 detail은 notFound()로 처리합니다. API가 인식하는 detail 404 reason은 not_found, not_published입니다. 기능 거부(feature_disabled)는 403이며 tenant mismatch, 무관한 배포 404와 함께 여전히 throw하며 읽기 오류 boundary로 전파합니다.

선택 URL과 공개 재고 경계

새 outbound URL은 stable ID를 사용합니다. 완전 선택은 variant=<variantId>, 부분 선택은 opt.<optionId>=<valueId>입니다. 이전 slug 기반 option URL은 호환 입력이며 label·slug가 canonical identity는 아닙니다. option key를 만들어내지 말고 실제 detail의 ID를 사용하세요. 이 페이지는 중복 query 값을 무시합니다. 두 레시피 모두 canonical-id를 명시하며 SDK 기본값은 slug-compat입니다. stale·알 수 없는·모호한 선택 query는 최신 detail의 빈 선택으로 초기화하고 깨끗한 Selection link를 만듭니다. 네트워크 재시도 UI로 보내지 않습니다. 구매 전 선택을 다시 해석하세요.

Catalog detail은 published, unlisted를 허용하며 raw 재고 수량은 제외합니다. 선택 availability는 표시 힌트입니다. 내부 재고를 표시하거나 구매 가능한 variant를 예약된 것으로 취급하지 마세요. 수량 판단은 stock check와 checkout을 사용합니다.

다음 액션

장바구니·checkout 후속 작업은 Ecommerce, media·cache resource·선택 helper는 SDK를 따릅니다. 특권 재고 조회는 이 catalog 레시피 범위 밖이며 trusted server와 server credential을 사용합니다.

목차