@sayren/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/index.mjs +361 -0
  2. package/package.json +42 -0
  3. package/template/.env +2 -0
  4. package/template/README.md +42 -0
  5. package/template/app/app.css +22 -0
  6. package/template/app/components/product-card.tsx +45 -0
  7. package/template/app/components/product-description.tsx +28 -0
  8. package/template/app/components/site-header.tsx +25 -0
  9. package/template/app/lib/api.server.ts +34 -0
  10. package/template/app/lib/api.ts +7 -0
  11. package/template/app/lib/cart-session.server.ts +18 -0
  12. package/template/app/lib/checkout-intent.ts +30 -0
  13. package/template/app/lib/config.server.ts +19 -0
  14. package/template/app/lib/config.test.ts +16 -0
  15. package/template/app/lib/config.ts +19 -0
  16. package/template/app/lib/format.ts +10 -0
  17. package/template/app/lib/payment-popup.ts +98 -0
  18. package/template/app/lib/session.server.ts +24 -0
  19. package/template/app/root.tsx +57 -0
  20. package/template/app/routes/cart.tsx +161 -0
  21. package/template/app/routes/checkout-complete.tsx +66 -0
  22. package/template/app/routes/checkout.tsx +251 -0
  23. package/template/app/routes/home.tsx +67 -0
  24. package/template/app/routes/login.tsx +52 -0
  25. package/template/app/routes/order-detail.tsx +76 -0
  26. package/template/app/routes/orders.tsx +48 -0
  27. package/template/app/routes/product-detail.tsx +153 -0
  28. package/template/app/routes/products.tsx +117 -0
  29. package/template/app/routes.ts +17 -0
  30. package/template/package.json +37 -0
  31. package/template/react-router.config.ts +6 -0
  32. package/template/tsconfig.json +16 -0
  33. package/template/vite.config.ts +7 -0
  34. package/template/vitest.config.ts +5 -0
@@ -0,0 +1,117 @@
1
+ import { Form, Link, useSearchParams } from "react-router";
2
+ import { ProductCard } from "../components/product-card";
3
+ import { apiFor } from "../lib/api.server";
4
+ import type { Route } from "./+types/products";
5
+
6
+ export function meta() {
7
+ return [{ title: "전체 상품" }];
8
+ }
9
+
10
+ /** 목록·검색 — 필터 상태는 URL에 둔다. 공유·뒤로가기가 그대로 동작한다 */
11
+ export async function loader({ request }: Route.LoaderArgs) {
12
+ const url = new URL(request.url);
13
+ const params = {
14
+ keyword: url.searchParams.get("keyword") ?? undefined,
15
+ categoryId: url.searchParams.get("categoryId") ?? undefined,
16
+ sort: (url.searchParams.get("sort") ?? undefined) as "recommend" | "latest" | undefined,
17
+ page: Number(url.searchParams.get("page") ?? 1),
18
+ size: 24,
19
+ };
20
+ const api = apiFor(request);
21
+ const [categories, page] = await Promise.all([
22
+ api.catalog.listCategories(),
23
+ api.catalog.searchProducts(params),
24
+ ]);
25
+ return { categories, page, applied: params };
26
+ }
27
+
28
+ export default function Products({ loaderData }: Route.ComponentProps) {
29
+ const { categories, page, applied } = loaderData;
30
+ const [searchParams] = useSearchParams();
31
+
32
+ return (
33
+ <div className="space-y-6">
34
+ <Form method="get" className="flex gap-2">
35
+ <input
36
+ type="search"
37
+ name="keyword"
38
+ defaultValue={applied.keyword ?? ""}
39
+ placeholder="상품을 검색해요"
40
+ className="h-11 flex-1 rounded-md border border-line px-3"
41
+ />
42
+ {applied.categoryId ? (
43
+ <input type="hidden" name="categoryId" value={applied.categoryId} />
44
+ ) : null}
45
+ <button type="submit" className="h-11 rounded-md bg-ink px-5 font-semibold text-white">
46
+ 검색
47
+ </button>
48
+ </Form>
49
+
50
+ <div className="flex flex-wrap gap-2">
51
+ <Link
52
+ to="/products"
53
+ className={`rounded-full px-3 py-1.5 text-sm ${applied.categoryId ? "bg-chip" : "bg-ink text-white"}`}
54
+ >
55
+ 전체
56
+ </Link>
57
+ {categories.map((category) => (
58
+ <Link
59
+ key={category.categoryId}
60
+ to={`/products?categoryId=${category.categoryId}`}
61
+ className={`rounded-full px-3 py-1.5 text-sm ${
62
+ applied.categoryId === category.categoryId ? "bg-ink text-white" : "bg-chip"
63
+ }`}
64
+ >
65
+ {category.name}
66
+ </Link>
67
+ ))}
68
+ </div>
69
+
70
+ {page.contents.length === 0 ? (
71
+ <p className="py-16 text-center text-muted">조건에 맞는 상품이 없어요</p>
72
+ ) : (
73
+ <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
74
+ {page.contents.map((product) => (
75
+ <ProductCard key={product.productId} product={product} />
76
+ ))}
77
+ </div>
78
+ )}
79
+
80
+ <Pagination page={page.page} totalPages={page.totalPages} searchParams={searchParams} />
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function Pagination({
86
+ page,
87
+ totalPages,
88
+ searchParams,
89
+ }: {
90
+ page: number;
91
+ totalPages: number;
92
+ searchParams: URLSearchParams;
93
+ }) {
94
+ if (totalPages <= 1) return null;
95
+ const linkTo = (next: number) => {
96
+ const params = new URLSearchParams(searchParams);
97
+ params.set("page", String(next));
98
+ return `/products?${params.toString()}`;
99
+ };
100
+ return (
101
+ <nav className="flex items-center justify-center gap-3 text-sm">
102
+ {page > 1 ? (
103
+ <Link to={linkTo(page - 1)} className="rounded-md border border-line px-3 py-1.5">
104
+ 이전
105
+ </Link>
106
+ ) : null}
107
+ <span className="text-muted">
108
+ {page} / {totalPages}
109
+ </span>
110
+ {page < totalPages ? (
111
+ <Link to={linkTo(page + 1)} className="rounded-md border border-line px-3 py-1.5">
112
+ 다음
113
+ </Link>
114
+ ) : null}
115
+ </nav>
116
+ );
117
+ }
@@ -0,0 +1,17 @@
1
+ import { index, type RouteConfig, route } from "@react-router/dev/routes";
2
+
3
+ /**
4
+ * 라우트 구성 — 구매 흐름 전체를 덮는다.
5
+ * 확장 지점: 기획전·브랜드관 같은 화면은 여기에 route()를 더하면 된다.
6
+ */
7
+ export default [
8
+ index("routes/home.tsx"),
9
+ route("products", "routes/products.tsx"),
10
+ route("products/:productId", "routes/product-detail.tsx"),
11
+ route("cart", "routes/cart.tsx"),
12
+ route("checkout", "routes/checkout.tsx"),
13
+ route("checkout/complete", "routes/checkout-complete.tsx"),
14
+ route("login", "routes/login.tsx"),
15
+ route("orders", "routes/orders.tsx"),
16
+ route("orders/:orderId", "routes/order-detail.tsx"),
17
+ ] satisfies RouteConfig;
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@sayren/template-storefront-react-router",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "react-router dev --port 4010",
8
+ "build": "react-router build",
9
+ "start": "react-router-serve ./build/server/index.js",
10
+ "lint": "biome check --write . && tsc --noEmit",
11
+ "typecheck": "react-router typegen && tsc --noEmit",
12
+ "test": "vitest run"
13
+ },
14
+ "dependencies": {
15
+ "@avarlabs/editor": "^0.1.0",
16
+ "@react-router/node": "^8.4.0",
17
+ "@react-router/serve": "^8.4.0",
18
+ "@sayren/storefront-sdk": "workspace:*",
19
+ "@tailwindcss/vite": "^4.3.3",
20
+ "isbot": "^5.1.0",
21
+ "react": "^19.3.0",
22
+ "react-dom": "^19.3.0",
23
+ "react-router": "^8.4.0",
24
+ "tailwindcss": "^4.3.3",
25
+ "zod": "catalog:"
26
+ },
27
+ "devDependencies": {
28
+ "@biomejs/biome": "catalog:",
29
+ "@react-router/dev": "^8.4.0",
30
+ "@types/node": "^24.13.5",
31
+ "@types/react": "^19.3.0",
32
+ "@types/react-dom": "^19.3.0",
33
+ "typescript": "catalog:",
34
+ "vite": "^8.3.0",
35
+ "vitest": "catalog:"
36
+ }
37
+ }
@@ -0,0 +1,6 @@
1
+ import type { Config } from "@react-router/dev/config";
2
+
3
+ export default {
4
+ // SSR을 끄지 않는다 — 상품 데이터를 서버에서 먼저 받아야 검색 노출과 첫 페인트가 산다.
5
+ ssr: true,
6
+ } satisfies Config;
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "types": ["node", "vite/client"],
12
+ "rootDirs": [".", "./.react-router/types"],
13
+ "paths": { "~/*": ["./app/*"] }
14
+ },
15
+ "include": ["**/*.ts", "**/*.tsx", ".react-router/types/**/*"]
16
+ }
@@ -0,0 +1,7 @@
1
+ import { reactRouter } from "@react-router/dev/vite";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import { defineConfig } from "vite";
4
+
5
+ export default defineConfig({
6
+ plugins: [tailwindcss(), reactRouter()],
7
+ });
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: { environment: "node", include: ["app/**/*.test.ts"] },
5
+ });