@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.
- package/dist/index.mjs +361 -0
- package/package.json +42 -0
- package/template/.env +2 -0
- package/template/README.md +42 -0
- package/template/app/app.css +22 -0
- package/template/app/components/product-card.tsx +45 -0
- package/template/app/components/product-description.tsx +28 -0
- package/template/app/components/site-header.tsx +25 -0
- package/template/app/lib/api.server.ts +34 -0
- package/template/app/lib/api.ts +7 -0
- package/template/app/lib/cart-session.server.ts +18 -0
- package/template/app/lib/checkout-intent.ts +30 -0
- package/template/app/lib/config.server.ts +19 -0
- package/template/app/lib/config.test.ts +16 -0
- package/template/app/lib/config.ts +19 -0
- package/template/app/lib/format.ts +10 -0
- package/template/app/lib/payment-popup.ts +98 -0
- package/template/app/lib/session.server.ts +24 -0
- package/template/app/root.tsx +57 -0
- package/template/app/routes/cart.tsx +161 -0
- package/template/app/routes/checkout-complete.tsx +66 -0
- package/template/app/routes/checkout.tsx +251 -0
- package/template/app/routes/home.tsx +67 -0
- package/template/app/routes/login.tsx +52 -0
- package/template/app/routes/order-detail.tsx +76 -0
- package/template/app/routes/orders.tsx +48 -0
- package/template/app/routes/product-detail.tsx +153 -0
- package/template/app/routes/products.tsx +117 -0
- package/template/app/routes.ts +17 -0
- package/template/package.json +37 -0
- package/template/react-router.config.ts +6 -0
- package/template/tsconfig.json +16 -0
- package/template/vite.config.ts +7 -0
- 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,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
|
+
}
|