상품 상세 페이지
Async route params로 상품 상세와 product selection을 표시합니다.
준비
상품 목록과 같은 키, 마켓, 콘텐츠 언어를 사용하세요. 이 페이지는 Next.js 16의 Promise params, searchParams를 사용하며 외부 ProductView 컴포넌트가 필요 없습니다. 목록 레시피의 읽기 오류 경계를 app/products/error.tsx에 추가하세요.
예제는 Next.js App Router 앱에서 요청마다 실행됩니다. 확인한 버전과 구성은 빌드 옵션에 있습니다.
상품 상세와 선택 표시
// 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>
)
}정상·빈 결과·실패
상품이 있으면 제목과 정규화된 상품 선택 링크를 표시합니다. 목록의 색상 견본 URL을 따라가면 상세 codec이 쿼리를 읽고 변형이나 부분 선택을 해석합니다. codec.stringify()는 ? 없이 쿼리를 반환하므로 예제는 필요할 때만 구분자를 붙입니다.
| 결과 | 페이지 동작 |
|---|---|
없음(not_found) 또는 공개되지 않음(not_published) | notFound()를 호출합니다. |
기능 꺼짐(feature_disabled) 또는 워크스페이스 불일치 | 403을 던지고, 읽기 오류 경계가 표시합니다. |
| 그 밖의 404(예: 잘못된 배포 URL) | 예외를 던지고, 읽기 오류 경계가 표시합니다. |
선택 URL과 공개 재고 경계
새 링크는 안정적인 ID를 씁니다. 전체 선택은 variant=<variantId>, 부분 선택은 opt.<optionId>=<valueId>입니다. 예전의 slug 기반 옵션 URL도 계속 읽습니다. 라벨과 slug는 표준 선택 식별자가 아닙니다. 옵션 키를 지어내지 말고 실제 상세 응답의 ID를 사용하세요. 이 페이지는 중복된 쿼리 값을 무시합니다. 두 레시피는 canonical-id를 지정하며, SDK 기본값은 slug-compat입니다. 오래되었거나 알 수 없거나 모호한 선택은 최신 상세 기준의 빈 선택으로 초기화되고 깨끗한 선택 링크가 만들어집니다. 네트워크 재시도 화면은 표시하지 않습니다. 구매 전에 선택을 다시 해석하세요.
카탈로그 상세는 published, unlisted 상품을 원시 재고 수량 없이 돌려줍니다. 선택의 구매 가능 여부는 표시용 힌트입니다. 내부 재고 필드를 표시하거나 구매 가능한 변형을 예약된 것으로 취급하지 마세요. 수량에 따라 달라지는 판단은 재고 확인과 Checkout으로 하세요.
다음 액션
Cart, Checkout, PortOne Standard V2 결제, 주문 확인은 Ecommerce 비회원 구매 완결로 이어집니다. 미디어는 갤러리 이미지와 영상 아이템, 캐시 무효화는 Storefront Cache Resources를 참고하세요. 권한이 필요한 재고 조회는 서버 자격 증명을 쓰는 신뢰할 수 있는 서버에서 하며, 이 카탈로그 레시피에서는 다루지 않습니다.