상품 카탈로그: 옵션 변형, 최소 단위 가격, 카테고리, 컬렉션, 번역, 안전한 재고 예약
설치
genpm add @core/catalog포함 내용
- src/lib/catalog/에 소스 코드, 파일 8개. (38.6kB)
- src/lib/catalog/AGENTS.md에 AI 규칙, 그리고 IDE 규칙 파일.
- .env.example에 추가되는 환경 변수: STORE_CURRENCY.
- @core/contracts, @core/db, @core/money, @core/rich-text을(를) 자동으로 해결합니다.
README
이 패키지에는 README가 없습니다.
이것이 AI가 src/lib/catalog에서 작업할 때 읽는 내용 그대로입니다. 그 외에는 컨텍스트에 아무것도 추가되지 않습니다.
@core/catalog — rules for AI agents
Purpose
Products for a store: up to 3 options (Size, Color…) and their variants with SKU, price/compare-at/cost in minor
units, stock with backorder rules, weight and image; categories (tree), manual ordered collections, tags, attributes,
per-locale translations, media references (@core/media ids), sitemap/search sources and admin resources.
reserveStock locks the variant row so two buyers can't get the last unit. No cart, prices rules or multi-warehouse.
Map
index.ts— public API:createProduct,updateProduct,getProduct,listProducts,reserveStock,releaseStock,upsertCategory,setCollection,categoryTree,lowStockVariants,catalogAdminResources.products.ts— products, variants, listing, stock.taxonomy.ts— categories and collections.sources.ts—productSitemapSource,productSearchSource.admin.ts— admin resources.schema.ts— tables.
Integration
- Env:
STORE_CURRENCY(ISO code, defaultEUR). Migrations as insrc/lib/db/AGENTS.md. - Product page:
const p = await getProduct(slug, { locale })(404 when null); showp.variantswithformat(v.price, locale)(@core/money) andv.availability; renderp.descriptionwith @core/rich-text. - Listing:
listProducts({ category, tag, collection, sort, page, minPrice, maxPrice, inStock }). - Register
productSitemapSource()(seo),productSearchSource()(search) and...catalogAdminResources()(admin). - JSON-LD with
jsonLd.product(@core/seo) usingavailabilityand the real price. - Verify: create a product with two variants in the admin and see it at
/products/<slug>.
Conventions
- Prices are integers in
STORE_CURRENCY; the server price is the only valid price (carts recompute it). - Reserve stock only inside the order transaction (
reserveStock(id, qty, tx)); release it on cancel/expiry. - Archive products instead of deleting them: orders keep references to their variants.
- Permissions:
products:read|create|update|delete.
Don't
- Don't show a compare-at price that isn't real (the EU requires the lowest price of the last 30 days for discounts).
- Don't decrement
stockwith plain updates outsidereserveStock/releaseStock. - Don't expose
costAmountin public pages or APIs.
# @core/catalog — rules for AI agents
## Purpose
Products for a store: up to 3 options (Size, Color…) and their variants with SKU, price/compare-at/cost in minor
units, stock with backorder rules, weight and image; categories (tree), manual ordered collections, tags, attributes,
per-locale translations, media references (@core/media ids), sitemap/search sources and admin resources.
`reserveStock` locks the variant row so two buyers can't get the last unit. No cart, prices rules or multi-warehouse.
## Map
- `index.ts` — public API: `createProduct`, `updateProduct`, `getProduct`, `listProducts`, `reserveStock`, `releaseStock`, `upsertCategory`, `setCollection`, `categoryTree`, `lowStockVariants`, `catalogAdminResources`.
- `products.ts` — products, variants, listing, stock. `taxonomy.ts` — categories and collections.
- `sources.ts` — `productSitemapSource`, `productSearchSource`. `admin.ts` — admin resources. `schema.ts` — tables.
## Integration
1. Env: `STORE_CURRENCY` (ISO code, default `EUR`). Migrations as in `src/lib/db/AGENTS.md`.
2. Product page: `const p = await getProduct(slug, { locale })` (404 when null); show `p.variants` with
`format(v.price, locale)` (@core/money) and `v.availability`; render `p.description` with @core/rich-text.
3. Listing: `listProducts({ category, tag, collection, sort, page, minPrice, maxPrice, inStock })`.
4. Register `productSitemapSource()` (seo), `productSearchSource()` (search) and `...catalogAdminResources()` (admin).
5. JSON-LD with `jsonLd.product` (@core/seo) using `availability` and the real price.
6. Verify: create a product with two variants in the admin and see it at `/products/<slug>`.
## Conventions
- Prices are integers in `STORE_CURRENCY`; the server price is the only valid price (carts recompute it).
- Reserve stock only inside the order transaction (`reserveStock(id, qty, tx)`); release it on cancel/expiry.
- Archive products instead of deleting them: orders keep references to their variants.
- Permissions: `products:read|create|update|delete`.
## Don't
- Don't show a compare-at price that isn't real (the EU requires the lowest price of the last 30 days for discounts).
- Don't decrement `stock` with plain updates outside `reserveStock`/`releaseStock`.
- Don't expose `costAmount` in public pages or APIs.
.genpmignore 적용 후 주입될 정확한 트리입니다. 고정 대상:
// Categorías (árbol) y colecciones manuales ordenadas.
import { asc, eq } from 'drizzle-orm';
import { z } from 'zod';
import { type Executor, getDb, withTransaction } from '../db/index.ts';
import { CatalogError } from './products.ts';
import { type Category, categories, collectionProducts, collections, products } from './schema.ts';
const Slug = z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(120);
export const CategoryInput = z.object({
slug: Slug,
name: z.string().min(1).max(120),
description: z.string().max(1000).nullish(),
parent: Slug.nullish(),
position: z.number().int().default(0),
translations: z.record(z.string(), z.object({ name: z.string().max(120).optional(), description: z.string().max(1000).optional() })).default({}),
});
/** Crea o actualiza (por slug) una categoría. Rechaza ciclos en el árbol. */
export async function upsertCategory(input: z.input<typeof CategoryInput>, db: Executor = getDb()): Promise<Category> {
const c = CategoryInput.parse(input);
let parentId: string | null = null;
if (c.parent) {
const all = await db.select().from(categories);
const parent = all.find((x) => x.slug === c.parent);
if (!parent) throw new CatalogError('invalid', `unknown parent ${c.parent}`);
for (let cur: Category | undefined = parent; cur; cur = all.find((x) => x.id === cur!.parentId))
if (cur.slug === c.slug) throw new CatalogError('invalid', 'category cycle');
parentId = parent.id;
}
const values = { slug: c.slug, name: c.name, description: c.description ?? null, parentId, position: c.position, translations: c.translations };
const [row] = await db.insert(categories).values(values).onConflictDoUpdate({ target: categories.slug, set: values }).returning();
return row!;
}
export type CategoryNode = Category & { children: CategoryNode[] };
/** Árbol completo de categorías ordenado por `position`. */
export async function categoryTree(db: Executor = getDb()): Promise<CategoryNode[]> {
const all = await db.select().from(categories).orderBy(asc(categories.position), asc(categories.name));
const nodes = new Map(all.map((c) => [c.id, { ...c, children: [] as CategoryNode[] }]));
const roots: CategoryNode[] = [];
for (const n of nodes.values()) (n.parentId && nodes.get(n.parentId) ? nodes.get(n.parentId)!.children : roots).push(n);
return roots;
}
/** Crea o actualiza una colección y fija sus productos en el orden dado (por slug de producto). */
export async function setCollection(input: { slug: string; name: string; description?: string | null; products: string[] }, db = getDb()) {
const slug = Slug.parse(input.slug);
return withTransaction(async (tx) => {
const values = { slug, name: input.name.slice(0, 120), description: input.description ?? null };
const [col] = await tx.insert(collections).values(values).onConflictDoUpdate({ target: collections.slug, set: values }).returning();
await tx.delete(collectionProducts).where(eq(collectionProducts.collectionId, col!.id));
const rows = await tx.select({ id: products.id, slug: products.slug }).from(products);
const items = input.products.map((s, position) => {
const p = rows.find((r) => r.slug === s);
if (!p) throw new CatalogError('invalid', `unknown product ${s}`);
return { collectionId: col!.id, productId: p.id, position };
});
if (items.length) await tx.insert(collectionProducts).values(items);
return col!;
}, db);
}
이 패키지는 MCP 서버를 선언하지 않습니다.
| 버전 | 커밋 | 게시일 | 검사 |
|---|---|---|---|
| 1.0.0 | 79d3468 | 3시간 전 | 검사 통과 |
- npm
- zod ^4.0.0
- 제안됨
- GenPM은 npm 명령을 제안하고, 동의한 경우에만 실행합니다.
- 검사
- 검사 통과 · 문제 0건
- 커밋
- v1.0.0 → 79d3468925acbde57d17a18a32e9d4bdefc28b5e · 가져온 뒤 검증됨
- 스크립트
- 없음. GenPM은 패키지 코드를 절대 실행하지 않습니다.
- 라이선스
- MIT
- 품질
- 100/100
- 인정된 라이선스충족
- AGENTS.md에 목적 설명충족
- AGENTS.md에 통합 단계충족
- AGENTS.md에 규칙 또는 금지 사항충족
- 테스트 포함충족
- 보안 검사 통과충족
- 최근 6개월 내 게시충족
- 인증된 게시자충족
- 요약과 키워드충족
- 신고
- 문제가 있나요?