@sayren/mcp 0.6.1 → 0.8.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 +6 -4
- package/package.json +3 -3
- package/template/.template-meta.json +2 -2
- package/template/README.md +27 -0
- package/template/package.json +6 -0
- package/template/src/components/fulfillment-codes.tsx +110 -0
- package/template/src/components/fulfillment-downloads.tsx +88 -0
- package/template/src/components/text-field.tsx +71 -0
- package/template/src/lib/catalog-queries.ts +47 -0
- package/template/src/lib/code-reveal.test.ts +30 -0
- package/template/src/lib/code-reveal.ts +32 -0
- package/template/src/lib/coupon-code.test.ts +19 -0
- package/template/src/lib/coupon-code.ts +60 -0
- package/template/src/lib/download.test.ts +41 -0
- package/template/src/lib/download.ts +41 -0
- package/template/src/lib/form-schemas.test.ts +144 -0
- package/template/src/lib/form-schemas.ts +131 -0
- package/template/src/lib/order-status.test.ts +29 -0
- package/template/src/lib/order-status.ts +28 -2
- package/template/src/lib/products-search.test.ts +25 -0
- package/template/src/lib/products-search.ts +49 -0
- package/template/src/router.tsx +26 -1
- package/template/src/routes/__root.tsx +9 -4
- package/template/src/routes/auth.callback.ts +13 -9
- package/template/src/routes/cart.tsx +11 -5
- package/template/src/routes/checkout.index.tsx +439 -146
- package/template/src/routes/guest-order.tsx +161 -71
- package/template/src/routes/orders.$orderId.tsx +94 -2
- package/template/src/routes/products.index.tsx +44 -46
- package/template/src/routes/signup.tsx +59 -89
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
1
2
|
import { ApiError, signupRequestSchema } from "@sayren/storefront-sdk";
|
|
2
3
|
import { createFileRoute, Link, redirect } from "@tanstack/react-router";
|
|
3
4
|
import { createServerFn } from "@tanstack/react-start";
|
|
4
|
-
import {
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { useForm } from "react-hook-form";
|
|
5
7
|
import { z } from "zod";
|
|
6
8
|
import { SubmitButton } from "../components/submit-button";
|
|
9
|
+
import { TextField } from "../components/text-field";
|
|
7
10
|
import { apiFor, authFor } from "../lib/api.server";
|
|
8
11
|
import { clearCartToken, readCartToken } from "../lib/cart-session.server";
|
|
12
|
+
import { type SignupForm, type SignupFormInput, signupFormSchema } from "../lib/form-schemas";
|
|
9
13
|
import { pageTitle } from "../lib/page-title";
|
|
10
14
|
import { safeRedirect } from "../lib/safe-redirect";
|
|
11
15
|
import { signIn } from "../lib/session.server";
|
|
@@ -90,57 +94,72 @@ export const Route = createFileRoute("/signup")({
|
|
|
90
94
|
function Signup() {
|
|
91
95
|
const { redirectTo } = Route.useSearch();
|
|
92
96
|
const [error, setError] = useState<string | null>(null);
|
|
93
|
-
const [
|
|
97
|
+
const [leaving, setLeaving] = useState(false);
|
|
98
|
+
const form = useForm<SignupFormInput, unknown, SignupForm>({
|
|
99
|
+
resolver: zodResolver(signupFormSchema),
|
|
100
|
+
// 기본값을 주지 않는다 — 주면 등록할 때 DOM을 덮어 하이드레이션 전에 입력·체크한 값이 사라진다.
|
|
101
|
+
// 체크박스는 DOM에서 boolean으로 읽힌다
|
|
102
|
+
});
|
|
103
|
+
const { errors, isSubmitting } = form.formState;
|
|
94
104
|
|
|
95
|
-
const submit = (
|
|
96
|
-
event.preventDefault();
|
|
97
|
-
const form = new FormData(event.currentTarget);
|
|
98
|
-
const text = (name: string) => String(form.get(name) ?? "");
|
|
99
|
-
setPending(true);
|
|
105
|
+
const submit = form.handleSubmit(async (values) => {
|
|
100
106
|
setError(null);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.then((result) => {
|
|
114
|
-
// 로그인 상태가 모든 화면에 새로 반영되도록 문서째 이동한다
|
|
115
|
-
if (result.redirectTo) window.location.assign(result.redirectTo);
|
|
116
|
-
else {
|
|
117
|
-
setError(result.error);
|
|
118
|
-
setPending(false);
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
.catch(() => {
|
|
122
|
-
setError("가입하지 못했습니다. 잠시 후 다시 시도해 주십시오");
|
|
123
|
-
setPending(false);
|
|
107
|
+
try {
|
|
108
|
+
const result = await signUp({
|
|
109
|
+
data: {
|
|
110
|
+
email: values.email,
|
|
111
|
+
password: values.password,
|
|
112
|
+
name: values.name,
|
|
113
|
+
phone: values.phone,
|
|
114
|
+
terms: values.agreements.terms,
|
|
115
|
+
privacy: values.agreements.privacy,
|
|
116
|
+
marketing: values.agreements.marketing,
|
|
117
|
+
redirectTo,
|
|
118
|
+
},
|
|
124
119
|
});
|
|
125
|
-
|
|
120
|
+
// 로그인 상태가 모든 화면에 새로 반영되도록 문서째 이동한다. 이동하는 동안 버튼을 다시 열지 않는다
|
|
121
|
+
if (result.redirectTo) {
|
|
122
|
+
setLeaving(true);
|
|
123
|
+
window.location.assign(result.redirectTo);
|
|
124
|
+
} else setError(result.error);
|
|
125
|
+
} catch {
|
|
126
|
+
setError("가입하지 못했습니다. 잠시 후 다시 시도해 주십시오");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const agreementError = errors.agreements?.terms?.message ?? errors.agreements?.privacy?.message;
|
|
126
130
|
|
|
127
131
|
return (
|
|
128
132
|
<div className="mx-auto max-w-sm space-y-5 py-10">
|
|
129
133
|
<h1 className="font-bold text-2xl">회원가입</h1>
|
|
130
134
|
<form onSubmit={submit} className="space-y-3">
|
|
131
|
-
<TextField
|
|
135
|
+
<TextField
|
|
136
|
+
label="이메일"
|
|
137
|
+
registration={form.register("email")}
|
|
138
|
+
error={errors.email?.message}
|
|
139
|
+
type="email"
|
|
140
|
+
autoComplete="email"
|
|
141
|
+
required
|
|
142
|
+
/>
|
|
132
143
|
<TextField
|
|
133
144
|
label="비밀번호"
|
|
134
|
-
|
|
145
|
+
registration={form.register("password")}
|
|
146
|
+
error={errors.password?.message}
|
|
135
147
|
type="password"
|
|
136
148
|
autoComplete="new-password"
|
|
137
149
|
required
|
|
138
150
|
hint="10자 이상, 영문·숫자·특수문자 중 2종 이상"
|
|
139
151
|
/>
|
|
140
|
-
<TextField
|
|
152
|
+
<TextField
|
|
153
|
+
label="이름"
|
|
154
|
+
registration={form.register("name")}
|
|
155
|
+
error={errors.name?.message}
|
|
156
|
+
autoComplete="name"
|
|
157
|
+
required
|
|
158
|
+
/>
|
|
141
159
|
<TextField
|
|
142
160
|
label="휴대폰 번호"
|
|
143
|
-
|
|
161
|
+
registration={form.register("phone")}
|
|
162
|
+
error={errors.phone?.message}
|
|
144
163
|
type="tel"
|
|
145
164
|
autoComplete="tel"
|
|
146
165
|
placeholder="01012345678"
|
|
@@ -149,21 +168,22 @@ function Signup() {
|
|
|
149
168
|
<fieldset className="space-y-2 rounded-md border border-line p-3 text-sm">
|
|
150
169
|
<legend className="px-1 font-medium">약관 동의</legend>
|
|
151
170
|
<label className="flex items-center gap-2">
|
|
152
|
-
<input type="checkbox"
|
|
171
|
+
<input type="checkbox" {...form.register("agreements.terms")} required />
|
|
153
172
|
이용약관 동의 (필수)
|
|
154
173
|
</label>
|
|
155
174
|
<label className="flex items-center gap-2">
|
|
156
|
-
<input type="checkbox"
|
|
175
|
+
<input type="checkbox" {...form.register("agreements.privacy")} required />
|
|
157
176
|
개인정보 수집이용 동의 (필수)
|
|
158
177
|
</label>
|
|
159
178
|
<label className="flex items-center gap-2">
|
|
160
|
-
<input type="checkbox"
|
|
179
|
+
<input type="checkbox" {...form.register("agreements.marketing")} />
|
|
161
180
|
마케팅 수신 동의 (선택)
|
|
162
181
|
</label>
|
|
182
|
+
{agreementError ? <p className="text-point text-xs">{agreementError}</p> : null}
|
|
163
183
|
</fieldset>
|
|
164
184
|
|
|
165
185
|
<SubmitButton
|
|
166
|
-
disabled={
|
|
186
|
+
disabled={isSubmitting || leaving}
|
|
167
187
|
className="h-12 w-full rounded-md bg-ink font-semibold text-white disabled:opacity-50"
|
|
168
188
|
>
|
|
169
189
|
가입하기
|
|
@@ -183,53 +203,3 @@ function Signup() {
|
|
|
183
203
|
</div>
|
|
184
204
|
);
|
|
185
205
|
}
|
|
186
|
-
|
|
187
|
-
function TextField({
|
|
188
|
-
label,
|
|
189
|
-
name,
|
|
190
|
-
type = "text",
|
|
191
|
-
autoComplete,
|
|
192
|
-
placeholder,
|
|
193
|
-
required,
|
|
194
|
-
hint,
|
|
195
|
-
}: {
|
|
196
|
-
label: string;
|
|
197
|
-
name: string;
|
|
198
|
-
type?: string;
|
|
199
|
-
autoComplete?: string;
|
|
200
|
-
placeholder?: string;
|
|
201
|
-
required?: boolean;
|
|
202
|
-
hint?: string;
|
|
203
|
-
}) {
|
|
204
|
-
const hintId = useId();
|
|
205
|
-
return (
|
|
206
|
-
<label className="block space-y-1">
|
|
207
|
-
<span className="text-sm">
|
|
208
|
-
{label}
|
|
209
|
-
{/* 필수 표시는 눈으로만 본다 — 입력의 required가 보조 기술에 필수임을 알린다 */}
|
|
210
|
-
{required ? (
|
|
211
|
-
<span aria-hidden="true" className="text-point">
|
|
212
|
-
{" "}
|
|
213
|
-
*
|
|
214
|
-
</span>
|
|
215
|
-
) : null}
|
|
216
|
-
</span>
|
|
217
|
-
<input
|
|
218
|
-
type={type}
|
|
219
|
-
name={name}
|
|
220
|
-
autoComplete={autoComplete}
|
|
221
|
-
placeholder={placeholder}
|
|
222
|
-
required={required}
|
|
223
|
-
// 이름은 라벨 글자 그대로다(필수 표시·안내 문구는 빼고) — 안내는 aria-describedby로 잇는다
|
|
224
|
-
aria-label={label}
|
|
225
|
-
aria-describedby={hint ? hintId : undefined}
|
|
226
|
-
className="h-11 w-full rounded-md border border-line px-3"
|
|
227
|
-
/>
|
|
228
|
-
{hint ? (
|
|
229
|
-
<span id={hintId} className="block text-muted text-xs">
|
|
230
|
-
{hint}
|
|
231
|
-
</span>
|
|
232
|
-
) : null}
|
|
233
|
-
</label>
|
|
234
|
-
);
|
|
235
|
-
}
|