실습형 안내서: Profound CMS에서 콘텐츠 기반 Stripe 스토어프런트를 구축합니다 — 상인이 코드 없이 편집하는 카탈로그, 두 개의 파라메트릭 라우트, 헤드리스 장바구니, Stripe 호스팅 체크아웃.
동작 중인 완성된 스토어 — 카테고리를 둘러보고, 상품을 열고, 장바구니에 담고, 결제하기.
Profound CMS 위에서 콘텐츠 기반 스토어를 구축하는 실습형 튜토리얼입니다. CMS에 모델링된 상품 카탈로그(카테고리 + 아이템), 하나의 라우트 세트에서 만들어지는 목록 및 상세 페이지, 그리고 헤드리스 컴포넌트로 제공되는 Stripe 호스팅 체크아웃을 다룹니다.
핵심은 손수 작성한 Next.js와 Profound 관리 도구입니다. Claude Code(Profound MCP를 통해)가 세 가지 작업—카탈로그 시드, 디자인 시스템 연결, 스토어프런트 컴포넌트(헤드리스 장바구니 포함) 작성—을 크게 도와줍니다. 세 부분으로 구성됩니다: 설정, 구축, 프로덕션.
한 줄로 끝나는 결제. 우리는 Stripe 호스팅 체크아웃을 사용합니다. 쇼핑객은 당신의 페이지가 아닌 Stripe 페이지에서 결제합니다. 앱이 서버 측에서 하는 일은 단 두 가지—Checkout 세션 생성과 웹훅 하나 검증뿐입니다. 카드 필드도, Stripe Elements도, PCI 부담도 없습니다.
/products/{item_code}, /categories/{category_code})와 정적 /cart — 모두 동일한 컴포넌트 세트에서 렌더링됩니다.useCart)와 Stripe 호스팅 체크아웃 — 가격은 항상 Stripe Price ID를 통해 서버 측에서 결정됩니다.curl -fsSL https://bun.sh/install | bashstripe login).gh)와 GitHub에 연결된 Vercel 계정.Profound는 콘텐츠와 렌더링을 분리합니다.
category, item). UI Element 태그가 붙은 컴포넌트는 페이지에 배치할 수 있습니다(nav, product_grid, …).meta.params.*, React에서는 routeParams).cms-renderer를 통해 콘텐츠를 읽고, Stripe는 일반 API 라우트로 추가합니다.구축을 좌우하는 단 하나의 규칙: CEL은 string/number 필드에만 바인딩됩니다. 따라서 네비게이션 브랜드, 푸터, 헤딩 같은 스칼라 크롬은 CEL로 바꾸고, 리치 콘텐츠나 컬렉션(상품 그리드, 이미지 갤러리, 리치 텍스트)은 React 컴포넌트 안에서 라우트 파라미터를 통해 가져옵니다. 그리고 Stripe가 가격의 단일 진실 공급원입니다. CMS의 price는 표시 용도일 뿐이며, 실제 청구 금액은 항상 Stripe Price ID를 사용해 서버 측에서 결정됩니다.
최종 상태: 게시된 작은 카탈로그, 그것을 읽도록 구성된 앱, Stripe 설치, 디자인 적용 — 아직 렌더링은 하지 않습니다.
Profound에 가입하세요(WorkOS 인증). store라는 이름으로 웹사이트를 만든 뒤, 웹사이트 ID(관리자 URL에 있는 UUID)와 읽기 티어 API 키(Deployments → Create API key)를 복사합니다. 앱은 읽기 전용으로만 사용합니다. 이후 카탈로그 시드는 MCP를 통해 진행하며, 별도로 인증합니다.
bunx create-profound-next store
cd store
bun add stripe
이 스캐폴드는 Profound에 맞춰 사전 구성된 Next.js App Router 프로젝트입니다(cms-renderer SDK, catch-all 라우트, generate-schemas 스크립트, <Refresher> 포함). 스타일은 없습니다. bun add stripe는 서버용 SDK를 설치합니다 — 호스팅 체크아웃에 필요한 결제 의존성은 이것뿐입니다.
.env.local에 값을 추가합니다.
# CMS
PROFOUND_API_KEY=<your read key>
NEXT_PUBLIC_PROFOUND_WEBSITE_ID=<your website id>
NEXT_PUBLIC_CMS_API_URL=https://cms.dev.tryprofound.com
NEXT_PUBLIC_BUNNY_CDN_URL=https://cms-profound.b-cdn.net # CMS 호스팅 이미지를 제공
# Stripe
STRIPE_SECRET_KEY=sk_test_... # 여기서는 테스트 키 사용; 라이브 전환 시 실키로 교체
STRIPE_WEBHOOK_SECRET=whsec_... # Build 4단계에서 채움
NEXT_PUBLIC_SITE_URL=http://localhost:3000
STRIPE_SECRET_KEY는 Stripe → Developers → API keys에서 가져옵니다. 빌드 과정에서 실제 결제가 발생하지 않도록 테스트 키(sk_test_…)를 사용합니다. 실제 결제를 받으려면 라이브 키로 교체하세요. bun dev를 실행하고 localhost:3000을 열면 시작 화면이 렌더링됩니다.
호스팅 체크아웃은 브라우저를 Stripe URL로 리디렉션하므로 서버 비밀 키만 있으면 됩니다. 퍼블리시어블 키나 클라이언트 Stripe SDK는 필요 없습니다.
category, product_image, item 컴포넌트 정의세 개의 커스텀 컴포넌트를 생성합니다(Components → Create new component). 데이터 소스이므로 UI Element 태그는 붙이지 않습니다. 각 컴포넌트를 Active로 설정합니다.
CMS에는 “이미지 배열” 필드가 없으므로 갤러리는 작은 product_image 컴포넌트를 참조하는 참조 배열로 구현합니다. category와 product_image를 먼저 만들어 Active로 설정한 뒤 item을 생성하세요. 참조 필드는 Active 컴포넌트만 대상으로 할 수 있습니다.
category — name(Text), code(Text, Route Slug), description(Rich text), heroImage(Image)product_image — image(Image)item — name(Text), code(Text, Route Slug), description(Rich text), images(참조 배열 → product_image), price(Number, 센트 단위 — 표시용), currency(Select, usd), stripePriceId(Text), category(Reference → category), active(Boolean)모든 필드는 선택 사항으로 둡니다. 관리자는 필드 이름을 스네이크 케이스로 변환합니다(“Stripe Price Id” → stripe_price_id). 코드에서 참조할 이름이므로 다음 단계에서 generate-schemas를 통해 실제 이름을 확인하세요. 라우트 핸들은 code로 지정합니다(slug 아님). 이는 Route Slug이자 나중에 깔끔한 documents.getByCode 조회 키가 됩니다.
item 컴포넌트 — Route Slug로 지정된 code, product_image를 참조하는 images, stripePriceId, category 참조를 포함합니다.
bun run generate-schemas
generated/cms-schemas.ts에 Zod 스키마와 타입(categorySchema/Category, itemSchema/Item)을 작성합니다. 연결 확인 용도로도 사용됩니다 — 자격 증명이 잘못되면 여기서 실패합니다.
MCP를 한 번 설치하고 인증합니다.
claude mcp add --transport http Profound http://107.21.107.99:8081/mcp
mcp__Profound__authenticate를 실행하고 WorkOS 인증 흐름을 완료한 뒤, Claude에게 다음과 같이 요청합니다.
Edison’s Inventions라는 스토어용 작은 이커머스 카탈로그를 만들어 주세요. 세 개의 카테고리와 아래 상품들에 대해, 각 상품마다 당시 분위기에 맞는 짧은
description, 센트 단위의price,currency: "usd",active: true가 필요합니다.
- Lighting & Power(
code: lighting): Incandescent Lightbulb(incandescent-lightbulb, $24), Electric Dynamo(electric-dynamo, $890), Electric Pen(electric-pen, $49)- Sound Recording(
code: sound): Tinfoil Phonograph(tinfoil-phonograph, $249), Carbon Microphone(carbon-microphone, $59), Dictaphone(dictaphone, $179)- Motion Pictures(
code: motion): Kinetoscope(kinetoscope, $399), Kinetograph Camera(kinetograph, $549)각 카테고리는
name과 소문자code가 필요합니다. 각 아이템에는name, 소문자code,description,price(센트),currency,active가 필요합니다.data/catalog.json에 저장하고 우리의category및item컴포넌트와 일치하는지 검증하세요. 그런 다음 Profound MCP를 사용해 각 문서를 게시 상태로 생성하세요. 카테고리를 먼저 만들고 ID를 기록한 뒤, 아이템 생성 시category를 참조로 연결합니다 —{ "_type": "reference", "_ref": "<category-id>", "_schema": "category" }. 아이템은 병렬로 생성해도 됩니다.
Claude는 data/catalog.json을 작성하고 검증한 뒤, 병렬 create_document 호출(status: "published")로 문서를 생성합니다. 참조가 이미 존재하는 ID를 가리키도록 카테고리를 먼저 시드하세요.
게시되고 Live 상태인 세 개의 카탈로그 카테고리.
각 카테고리에 연결된 여덟 개의 상품.
카탈로그는 CMS에 준비되었습니다. 이제 몇몇 상품에 실제 Stripe 가격을 연결합니다 — 상인은 두 개의 관리자 패널에서 코드 없이 수행합니다.
price_…)를 복사합니다.stripePriceId에 붙여 넣고 저장합니다.CMS는 카탈로그를, Stripe는 실제 가격을 보유합니다. 둘은 상인이 붙여 넣는 문자열 하나로 연결됩니다. 자동화를 선호한다면 공식 Stripe MCP로 Product/Price를 생성하고, 반환된 ID를 동일하게 붙여 넣을 수 있습니다.
스캐폴드는 스타일을 제공하지 않습니다. 프로젝트 루트에 DESIGN.md(Tailwind v4 @theme 블록 + 토큰)를 추가하세요 — 직접 만들거나 refero.design에서 다운로드할 수 있습니다. 그런 다음 Claude에게 스타일링 범위만 지정해 요청합니다.
방금 추가한 디자인 파일을 읽고, 필요하다면 Tailwind를 설정한 뒤 테마와 폰트를 연결해 스타일이 적용되도록 해 주세요. 폰트는
next/font를 사용하고, 런타임에 Google에서 불러오지 마세요. 스타일링만 처리하고 페이지나 컴포넌트는 아직 만들지 마세요.
src/app/globals.css에 @import "tailwindcss";와 @theme 블록이 있는지 확인하고, localhost:3000에서 토큰이 적용되는지 확인하세요. 프롬프트를 간결하게 유지하세요(너무 개방적이면 에이전트가 전체 홈페이지를 스캐폴딩할 수 있습니다). 폰트는 반드시 next/font로 로드하고, 런타임 Google import는 피하세요.
선택 사항입니다 — 이미지 없이도 작동하는 결제 흐름을 만들 수 있습니다. 추가하려면: 이미지마다 product_image 문서를 생성하고(image 필드에 업로드), 상품의 images 배열에서 참조합니다. 직접 촬영한 사진을 사용하거나, 이미지 모델로 일관된 세트를 생성하세요(Claude에게 DESIGN.md 기반 프롬프트와 통일된 Midjourney --sref를 지정하면 모든 이미지가 같은 톤을 유지합니다).
독립 실행형
cms-renderer에는 이미지 URL 헬퍼가 없으므로buildAssetUrl을src/lib/image.ts에 구현하세요(약 40줄). 이 함수는NEXT_PUBLIC_BUNNY_CDN_URL을 접두사로 붙이고 확장자를 추가합니다. 2부 3단계의 컴포넌트에서 사용합니다.
렌더링 레이어와 체크아웃을 구축해 테스트 모드 실제 구매까지 완료합니다.
다섯 개의 컴포넌트를 생성하고 각각 Active로 설정한 뒤 UI Element 태그를 붙입니다(Settings → Tags). Route Slug는 필요 없습니다.
nav → brandproduct_grid → headingproduct_detail → headingcart_summary → headingfooter → textUI Element 태그가 있어야 페이지 빌더의 Add UI Element 목록에 표시됩니다(Active만으로는 부족합니다). 각 필드는 스칼라이며(CEL이 바인딩할 수 있는 타입), 실제 카탈로그 데이터는 여기서 다루지 않습니다. ProductGrid 및 ProductDetail은 3단계에서 라우트 파라미터를 통해 데이터를 가져옵니다.
bun run generate-schemas
다음 프롬프트 하나로 읽기 헬퍼, 다섯 개 컴포넌트, 장바구니, 레지스트리를 생성합니다.
Profound
cms-rendererSDK를 사용해src/아래에 스토어프런트를 구축해 주세요.
src/lib/catalog.ts— 서버 측 CMS 리더입니다.cms-renderer/lib/cms-api에서getCmsClient({ cmsUrl: process.env.NEXT_PUBLIC_CMS_API_URL!, apiKey: process.env.PROFOUND_API_KEY, websiteId: process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID! })로 클라이언트를 만들고,getItemByCode(code)를 내보내세요. 이 함수는cms.documents.getByCode.query({ websiteId, schemaName: "item", code })를 호출하고 **res.document.published_content**를 반환해야 합니다.listItems(categoryCode?)도 내보내세요. 이는cms.documents.list.query({ websiteId, schemaName: "item", status: "published", limit: 100 })를 호출하고,res.documents를.published_content로 매핑한 뒤active !== false인 아이템만 남깁니다.categoryCode가 전달되면 참조가 해당 카테고리document.id와 일치하는 아이템만 유지합니다.resolveImages(refs)를 구현해 각item.images참조를cms.documents.get.query({ websiteId, id: ref._ref })로 가져오고,buildAssetUrl(1부 8단계)로 이미지 필드를 URL로 변환하세요.
src/components/— 다섯 개 UI 요소 컴포넌트를 catch-all 라우트의 레지스트리에 등록합니다. 관리자와 일치하도록 이름은 snake_case로 유지합니다:{ nav, product_grid, product_detail, cart_summary, footer }.Nav와Footer는contentprop에서 스칼라 필드를 읽습니다(타입은cms-renderer/lib/types의BlockComponentProps<T>).ProductGrid와ProductDetail은 async 서버 컴포넌트로,routeParams를 읽고catalog.ts에서 데이터를 가져옵니다:routeParams.<param>는{ value, … }이므로.value를 사용하세요.ProductGrid는listItems(routeParams.category_code?.value)를 호출해 카드 링크를/products/{code}로 렌더링합니다.ProductDetail은getItemByCode(routeParams.item_code?.value)를 호출해 갤러리(resolveImages), 리치 텍스트 설명, 가격, 장바구니 담기 버튼을 표시합니다.CartSummary는useCart로부터 장바구니를 렌더링하고 Pay 버튼을 제공합니다. 가격 포맷터는 순수 함수src/lib/format.ts로 분리해 클라이언트 컴포넌트가 서버 전용catalog.ts를 가져오지 않도록 합니다.
src/components/AddToCartButton.tsx—"use client"가 선언된 버튼으로{ code, name, priceLabel }을 받고useCart().addItem({ code, name, priceLabel, quantity: 1 })를 호출합니다.ProductDetail에서 사용하세요.
src/lib/useCart.ts— 헤드리스 장바구니입니다. 라인 아이템은{ code, name, priceLabel, quantity }를 상태로 보관하고localStorage에 저장합니다.addItem/removeItem/updateQty/subtotal과checkout()을 제공합니다.checkout()은{ lines: [{ code, quantity }] }(코드와 수량만 — 가격은 절대 포함하지 않음)을/api/stripe/checkout으로 POST한 뒤 반환된url로 리디렉션합니다.모든 컴포넌트는 디자인 시스템에 맞게, 우리만의 컴포넌트로 스타일링하고, 원본 사이트의 레이아웃을 복사하지 마세요.
프롬프트 실행 후 알아둘 세 가지:
content에 포함됩니다({ content }: BlockComponentProps<T>). 필드를 최상위 props로 디스트럭처링하면 블록이 비어 있을 수 있습니다. 카탈로그 데이터는 라우트 파라미터 + catalog.ts 호출로 가져옵니다. CEL은 리스트나 갤러리를 바인딩할 수 없기 때문입니다. 장바구니는 상품 코드만 유지하고, 가격은 유지하지 않습니다.routeParams.<param>는 { value, schemaName, document } 형태입니다 — .value를 사용하세요. 조회 결과는 .content가 아닌 **published_content**입니다. 레지스트리 키는 관리자와 일치하도록 snake_case여야 합니다.@types/react/@types/react-dom을 v19로 올리세요 — 스캐폴드는 v18을 사용하며, React 19에서 async 서버 컴포넌트 블록이 오류를 일으킵니다.앱에서 필요한 결제 코드는 세 개의 짧은 서버 파일뿐입니다. 모두 getItemByCode를 재사용해 서버 측에서 가격을 결정합니다.
src/lib/stripe.ts:
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
src/app/api/stripe/checkout/route.ts — 각 아이템을 CMS에서 조회하고 Stripe 가격으로 청구합니다.
import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getItemByCode } from "@/lib/catalog"; // 서버 측, 읽기 전용 키
export async function POST(req: Request) {
const { lines } = await req.json(); // [{ code, quantity }] — 클라이언트에서 가격 전달 금지
const line_items = await Promise.all(
lines.map(async ({ code, quantity }: { code: string; quantity: number }) => {
const item = await getItemByCode(code); // 서버에서 CMS 조회
return { price: item!.stripe_price_id, quantity }; // 가격은 CMS에서, 클라이언트 아님
})
);
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items,
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cart?status=success`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cart?status=cancelled`,
});
return NextResponse.json({ url: session.url }); // 클라이언트가 여기로 리디렉션
}
src/app/api/stripe/webhook/route.ts — 신뢰할 수 있는 완료 신호입니다.
import { stripe } from "@/lib/stripe";
export async function POST(req: Request) {
const body = await req.text(); // RAW 본문 — 서명 검증에 필요
const sig = req.headers.get("stripe-signature")!;
let event;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Bad signature", { status: 400 });
}
if (event.type === "checkout.session.completed") {
// 주문 이행: 주문 기록 / 영수증 발송 등 처리
}
return new Response(null, { status: 200 });
}
필수 스캐폴드 수정: 스캐폴드의
src/proxy.ts는 모든/api/*요청을 CMS로 전달하므로 Stripe 라우트가 작동하지 않습니다. 우선 해당 라우트가 통과하도록 변경하세요.import { createCmsProxy } from "cms-renderer/lib/proxy"; import { NextResponse, type NextRequest } from "next/server"; import { cmsConfig } from "@/lib/cms-config"; const cmsProxy = createCmsProxy({ upstream: cmsConfig.cmsUrl }); const LOCAL_API_PREFIXES = ["/api/stripe"]; export const proxy = async (request: NextRequest) => { if (LOCAL_API_PREFIXES.some((p) => request.nextUrl.pathname.startsWith(p))) { return NextResponse.next(); // 로컬에서 처리 } return cmsProxy(request as unknown as Parameters<typeof cmsProxy>[0]); }; // 스캐폴드의 `export const config = { matcher: [...] }`는 그대로 유지확인:
curl -X POST localhost:3000/api/stripe/webhook -d x실행 시Bad signature가 반환되어야 합니다.
Stripe CLI로 로컬 웹훅을 실행합니다.
stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# whsec_... 값을 STRIPE_WEBHOOK_SECRET에 복사하고 bun dev 재시작
whsec_…는 세션마다 다릅니다. 보안은 두 가지 규칙으로 보장됩니다. 체크아웃은 CMS에서 code로 가격을 다시 가져오므로 조작된 장바구니가 가격을 바꿀 수 없습니다. 웹훅은 원본(raw) 본문으로 서명을 검증합니다.
관리자 → Pages → Create page에서 세 번 생성합니다. 각 파라미터를 컴포넌트에 매핑하고 슬러그 필드는 code입니다.
/products/{item_code} → item/categories/{category_code} → category/cart — 정적 페이지(문자 그대로 /cart를 입력, /{cart} 아님)각 라우트에서 Page Builder → Add UI Element → Custom 순으로 컴포넌트를 추가하고, 스칼라 필드를 채운 뒤 Publish합니다.
/products/{item_code}: nav, product_detail, footer/categories/{category_code}: nav, product_grid, footer/cart: nav, cart_summary, footernav.brand와 footer.text는 정적 문자열로 설정하고, 헤딩도 정적 라벨을 사용하세요.
카테고리 라우트의 Page Builder — product_grid 요소를 선택하고 헤딩을 CEL로 바인딩한 모습.
상품 라우트의 Page Builder — product_detail 요소가 특정 상품에 바인딩된 상태.
파라메트릭 Page Builder 이슈: 두 파라메트릭 라우트에서 UI 요소를 추가하면 저장되지 않습니다(블록이 고아 상태가 되어 페이지가 비어 보입니다). 문제를 해결하기 전까지는 Profound MCP의
update_page를 사용해 해당 페이지의block_ids를 직접 연결한 뒤 게시하세요. (정적/cart는 정상적으로 연결됩니다.) 같은 이유로ProductGrid는 CEL 대신 가져온 카테고리에서 헤딩을 파생합니다.
/categories/lighting에서 그리드를 확인합니다. 상품을 클릭하면 상세 페이지, 장바구니 담기 버튼이 표시됩니다. /cart로 이동해 Pay 버튼을 확인하세요./cart?status=success로 돌아오고, stripe listen에는 checkout.session.completed가 표시됩니다.렌더링된 상품 페이지 — 갤러리, 가격, 장바구니 담기 버튼.
장바구니 — 라인 아이템과 Stripe 결제 버튼.
가격이 연결된 상품만 구매할 수 있습니다 — 6단계에서 Price ID를 입력한 상품 중 하나를 선택하세요.
선택 사항 — 국제화. 각 컴포넌트를 번역하고(35개 언어를 한 번에), 내장된
language시스템 컴포넌트에 매핑되는/{language}/…세그먼트를 추가한 뒤, CEL로 바인딩된 필드를documents.translated로 전환하세요. 자세한 내용은 공항 디렉터리 튜토리얼 2부 7단계를 참고하세요.
git init && git add -A && git commit -m "Stripe storefront"
gh repo create store --private --source=. --push # --public도 사용 가능
빌드 커맨드:
generated/cms-schemas.ts는.gitignore에 포함되어 있으므로 빌드 시 재생성이 필요합니다.vercel.json을 추가하세요.{ "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run generate-schemas && next build" }
Vercel에서 Add New → Project로 store 저장소를 가져오고 환경 변수를 추가합니다. PROFOUND_API_KEY, NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, NEXT_PUBLIC_BUNNY_CDN_URL, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET(아래에서 설정할 배포 엔드포인트용 값), NEXT_PUBLIC_SITE_URL(프로덕션 URL)을 설정한 뒤 배포합니다.
그다음 배포된 웹훅을 연결합니다(로컬 stripe listen 비밀키는 로컬 전용). Stripe → Developers → Webhooks → + Add endpoint → https://<prod>/api/stripe/webhook, 이벤트는 checkout.session.completed. 생성된 whsec_…를 Vercel에 추가하고 다시 배포합니다.
환경 변수가 없으면 “로컬에서는 되는데 프로덕션에서는 빈 화면”이 됩니다 — 가장 흔한 배포 오류입니다. 여기서는 테스트 키로 배포하지만, 실제 결제를 받을 준비가 되면 STRIPE_SECRET_KEY와 웹훅 비밀키를 라이브 값으로 교체하세요.
두 기능 모두 스캐폴드에 포함되어 있습니다.
<Refresher>는 관리자가 저장할 때 프리뷰 중인 페이지를 즉시 갱신합니다 — 재배포가 필요 없습니다. (이는 편집자를 위한 프리뷰이며, 일반 방문자는 게시된 콘텐츠를 정상적인 재검증 사이클로 보게 됩니다.)?edit_mode=true를 추가하면 편집 오버레이가 나타납니다. 일반 방문자는 깨끗한 페이지를 봅니다.스캐폴드에 빠진 프리뷰 라우트를 추가하세요. 관리자는 프리뷰 iframe을
/cms-preview_<path>에 로드합니다. 해당 라우트가 없으면 모든 프리뷰가 404가 됩니다. 다음을 추가하세요.// src/app/cms-preview_/[...slug]/page.tsx import { ParametricRoutePreviewPage } from "cms-renderer/lib/renderer"; import { registry } from "../../registry"; // 레지스트리를 공유 모듈로 추출 export default async function Page({ params, searchParams }) { const { slug } = await params; const PreviewPage = ParametricRoutePreviewPage as any; // async RSC; React 19 타입 return <PreviewPage registry={registry} apiKey={process.env.PROFOUND_API_KEY ?? ""} websiteId={process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID ?? ""} cmsUrl={process.env.NEXT_PUBLIC_CMS_API_URL ?? "https://cms.dev.tryprofound.com"} params={Promise.resolve({ slug })} searchParams={searchParams} />; }세그먼트 루트용
src/app/cms-preview_/page.tsx도 추가하세요(동일하되slug: []).
콘텐츠 기반 Stripe 스토어프런트가 완성되었습니다. CMS 카탈로그, 하나의 컴포넌트 세트로 구현한 목록 + 상세 페이지, 그리고 실제로 동작하는 호스팅 체크아웃입니다. AI가 카탈로그를 시드하고, 디자인을 연결하고, 카탈로그 리더 + 컴포넌트 + 헤드리스 장바구니까지 작성했습니다. 우리는 컴포넌트 배치, Stripe Price 연결, 세 개의 라우트 구성, CEL 크롬, 그리고 세 개의 짧은 Stripe 파일만 추가하면 됩니다. CEL은 크롬을 바인딩하고, 컴포넌트는 카탈로그를 가져옵니다. Stripe는 작고 단순하게 유지됩니다 — sessions.create 한 번과 서명된 웹훅 하나뿐이며, 쇼핑객은 Stripe 페이지에서 결제합니다.