@sayren/mcp 0.5.0 → 0.5.2
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 +31 -31
- package/package.json +3 -3
- package/template/.template-meta.json +2 -2
- package/template/README.md +3 -2
- package/template/src/lib/config.server.ts +1 -1
- package/template/src/lib/payments.ts +2 -1
- package/template/src/routes/__root.tsx +2 -2
- package/template/src/routes/account.tsx +14 -14
- package/template/src/routes/cart.tsx +3 -3
- package/template/src/routes/checkout.complete.tsx +2 -2
- package/template/src/routes/checkout.index.tsx +25 -21
- package/template/src/routes/checkout.return.tsx +7 -7
- package/template/src/routes/index.tsx +1 -1
- package/template/src/routes/login.tsx +9 -9
- package/template/src/routes/orders.$orderId.tsx +9 -9
- package/template/src/routes/orders.index.tsx +1 -1
- package/template/src/routes/products.$productId.tsx +1 -1
- package/template/src/routes/products.index.tsx +2 -2
package/dist/index.mjs
CHANGED
|
@@ -142,14 +142,14 @@ function buildPath(template, params) {
|
|
|
142
142
|
const used = /* @__PURE__ */ new Set();
|
|
143
143
|
const path = template.replace(/\{([^}]+)\}/g, (_match, name) => {
|
|
144
144
|
const value = params[name];
|
|
145
|
-
if (value === void 0 || value === "") throw new Error(`경로 값이
|
|
145
|
+
if (value === void 0 || value === "") throw new Error(`경로 값이 없습니다: ${name}`);
|
|
146
146
|
const text$1 = String(value);
|
|
147
|
-
if (text$1 === "." || text$1 === ".." || !SAFE_SEGMENT.test(text$1)) throw new Error(`쓸 수 없는 경로
|
|
147
|
+
if (text$1 === "." || text$1 === ".." || !SAFE_SEGMENT.test(text$1)) throw new Error(`쓸 수 없는 경로 값입니다: ${name}`);
|
|
148
148
|
used.add(name);
|
|
149
149
|
return encodeURIComponent(text$1);
|
|
150
150
|
});
|
|
151
151
|
const extra = Object.keys(params).filter((name) => !used.has(name));
|
|
152
|
-
if (extra.length) throw new Error(`경로에 없는
|
|
152
|
+
if (extra.length) throw new Error(`경로에 없는 값입니다: ${extra.join(", ")}`);
|
|
153
153
|
return path;
|
|
154
154
|
}
|
|
155
155
|
/**
|
|
@@ -163,7 +163,7 @@ function truncateData(data, maxChars) {
|
|
|
163
163
|
while (kept > 0 && JSON.stringify(rebuild(items.slice(0, kept))).length > maxChars) kept = Math.floor(kept / 2);
|
|
164
164
|
return {
|
|
165
165
|
data: rebuild(items.slice(0, kept)),
|
|
166
|
-
truncated: `목록 ${items.length}개 중 ${kept}개만
|
|
166
|
+
truncated: `목록 ${items.length}개 중 ${kept}개만 실었습니다. size·page로 나눠 조회하십시오`
|
|
167
167
|
};
|
|
168
168
|
};
|
|
169
169
|
if (Array.isArray(data)) return shrink(data, (kept) => kept);
|
|
@@ -175,29 +175,29 @@ function truncateData(data, maxChars) {
|
|
|
175
175
|
const text$1 = JSON.stringify(data);
|
|
176
176
|
return {
|
|
177
177
|
data: `${text$1.slice(0, maxChars)}…`,
|
|
178
|
-
truncated: `응답 ${text$1.length}자 중 ${maxChars}자만
|
|
178
|
+
truncated: `응답 ${text$1.length}자 중 ${maxChars}자만 실었습니다`
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
181
|
/** 같은 멱등키의 승인이 끝났을 때 상태별 안내 — 에이전트가 다음에 할 일 */
|
|
182
182
|
const DECIDED_GUIDE = {
|
|
183
183
|
SUCCEEDED: {
|
|
184
|
-
message: "이 멱등키의 요청은 셀러가 승인해서 이미
|
|
184
|
+
message: "이 멱등키의 요청은 셀러가 승인해서 이미 실행됐습니다. 다시 실행하지 않았습니다",
|
|
185
185
|
nextSteps: ["실행 결과는 `result`다. 같은 요청을 다시 보내지 않는다"]
|
|
186
186
|
},
|
|
187
187
|
FAILED: {
|
|
188
|
-
message: "이 멱등키의 요청은 승인 뒤 실행했지만
|
|
188
|
+
message: "이 멱등키의 요청은 승인 뒤 실행했지만 실패했습니다",
|
|
189
189
|
nextSteps: ["`result.error`로 원인을 사용자에게 알린다", "고쳐서 다시 하려면 새 멱등키로 요청한다(같은 키는 이 결과만 돌려준다)"]
|
|
190
190
|
},
|
|
191
191
|
UNKNOWN: {
|
|
192
|
-
message: "이 멱등키의 요청은 승인 뒤 실행 결과를 확인하지
|
|
192
|
+
message: "이 멱등키의 요청은 승인 뒤 실행 결과를 확인하지 못했습니다. 이미 실행됐을 수 있습니다",
|
|
193
193
|
nextSteps: ["다시 보내지 않는다. 대상 리소스를 조회해 바뀌었는지 확인하고 사용자에게 알린다", "셀러에게 콘솔의 감사 로그에서 실행 여부를 확인해 달라고 전한다"]
|
|
194
194
|
},
|
|
195
195
|
REJECTED: {
|
|
196
|
-
message: "이 멱등키의 요청은 셀러가
|
|
196
|
+
message: "이 멱등키의 요청은 셀러가 거절했습니다. 아무것도 바뀌지 않았습니다",
|
|
197
197
|
nextSteps: ["거절 사유(`decisionNote`)를 전한다. 사용자가 원하면 새 멱등키로 다시 요청한다"]
|
|
198
198
|
},
|
|
199
199
|
EXPIRED: {
|
|
200
|
-
message: "이 멱등키의 요청은 승인 기한(24시간)이
|
|
200
|
+
message: "이 멱등키의 요청은 승인 기한(24시간)이 지났습니다. 아무것도 바뀌지 않았습니다",
|
|
201
201
|
nextSteps: ["사용자가 원하면 새 멱등키로 다시 요청한다"]
|
|
202
202
|
}
|
|
203
203
|
};
|
|
@@ -231,7 +231,7 @@ function describeWriteResult(result) {
|
|
|
231
231
|
body: {
|
|
232
232
|
status: "APPROVAL_EXECUTING",
|
|
233
233
|
applied: "unknown",
|
|
234
|
-
message: "셀러가 승인해서 서버가 이 요청을 실행하고
|
|
234
|
+
message: "셀러가 승인해서 서버가 이 요청을 실행하고 있습니다",
|
|
235
235
|
approvalId: pending.approvalId,
|
|
236
236
|
nextSteps: ["같은 요청을 다시 보내지 않는다. 결과는 `get_approval_request`로 확인한다"]
|
|
237
237
|
}
|
|
@@ -241,7 +241,7 @@ function describeWriteResult(result) {
|
|
|
241
241
|
body: {
|
|
242
242
|
status: "APPROVAL_PENDING",
|
|
243
243
|
applied: false,
|
|
244
|
-
message: "승인 대기
|
|
244
|
+
message: "승인 대기 중입니다. 아직 아무것도 바뀌지 않았습니다. 셀러가 콘솔에서 승인하면 서버가 이 요청을 그대로 실행합니다",
|
|
245
245
|
approvalId: pending.approvalId,
|
|
246
246
|
reason: pending.reason,
|
|
247
247
|
expiresAt: pending.expiresAt,
|
|
@@ -256,7 +256,7 @@ function describeWriteResult(result) {
|
|
|
256
256
|
body: {
|
|
257
257
|
status: "DRY_RUN",
|
|
258
258
|
applied: false,
|
|
259
|
-
message: "실행하지
|
|
259
|
+
message: "실행하지 않았습니다. 서버의 판정과 영향 미리보기입니다",
|
|
260
260
|
...result.data
|
|
261
261
|
}
|
|
262
262
|
};
|
|
@@ -314,7 +314,7 @@ var AdminApi = class {
|
|
|
314
314
|
redirect: "error",
|
|
315
315
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
316
316
|
});
|
|
317
|
-
if (!response.ok) throw new Error(`API 문서를 읽지
|
|
317
|
+
if (!response.ok) throw new Error(`API 문서를 읽지 못했습니다 (HTTP ${response.status})`);
|
|
318
318
|
const document = await response.json();
|
|
319
319
|
this.spec = {
|
|
320
320
|
document,
|
|
@@ -326,7 +326,7 @@ var AdminApi = class {
|
|
|
326
326
|
async find(operationId) {
|
|
327
327
|
const { document, operations } = await this.load();
|
|
328
328
|
const op = operations.find((candidate) => candidate.operationId === operationId);
|
|
329
|
-
if (!op) throw new Error(`쓸 수 있는 오퍼레이션이
|
|
329
|
+
if (!op) throw new Error(`쓸 수 있는 오퍼레이션이 아닙니다: ${operationId}. list_operations의 operationId를 쓰십시오`);
|
|
330
330
|
return {
|
|
331
331
|
document,
|
|
332
332
|
op
|
|
@@ -372,7 +372,7 @@ var AdminApi = class {
|
|
|
372
372
|
const DEFAULT_API_ORIGIN = "https://api.sayren.app";
|
|
373
373
|
function loadConfig(env = process.env) {
|
|
374
374
|
const token = env.SAYREN_TOKEN?.trim();
|
|
375
|
-
if (!token) throw new Error("SAYREN_TOKEN이
|
|
375
|
+
if (!token) throw new Error("SAYREN_TOKEN이 없습니다. 셀러 콘솔 개발자 공간 › API 토큰에서 발급한 토큰을 MCP 설정의 env에 넣어주십시오");
|
|
376
376
|
const origin = (env.SAYREN_API_ORIGIN ?? DEFAULT_API_ORIGIN).replace(/\/+$/, "");
|
|
377
377
|
return {
|
|
378
378
|
apiOrigin: origin,
|
|
@@ -390,7 +390,7 @@ async function callApi(url, init = {}) {
|
|
|
390
390
|
const body = await response.json().catch(() => null);
|
|
391
391
|
if (!response.ok) {
|
|
392
392
|
const code = body?.error?.code ?? String(response.status);
|
|
393
|
-
const message = body?.error?.message ?? "요청이
|
|
393
|
+
const message = body?.error?.message ?? "요청이 실패했습니다";
|
|
394
394
|
throw new Error(`${code}: ${message}`);
|
|
395
395
|
}
|
|
396
396
|
return body?.data ?? body;
|
|
@@ -412,7 +412,7 @@ async function fetchStoreContext(config$1) {
|
|
|
412
412
|
const payment = paymentSettings ? {
|
|
413
413
|
connectedProviders: connected.map((p) => p.provider),
|
|
414
414
|
sandboxOnly: connected.length === 0 || connected.every((p) => p.sandboxMode)
|
|
415
|
-
} : { unavailable: "결제 설정은 셀러 콘솔 OWNER만 볼 수 있어 이 토큰으로는 확인하지
|
|
415
|
+
} : { unavailable: "결제 설정은 셀러 콘솔 OWNER만 볼 수 있어 이 토큰으로는 확인하지 못했습니다" };
|
|
416
416
|
return {
|
|
417
417
|
storeId: store.storeId,
|
|
418
418
|
storeCode: store.storeCode,
|
|
@@ -519,12 +519,12 @@ function resolveSpec(name, spec, meta, options) {
|
|
|
519
519
|
if (options.link && name.startsWith("@sayren/")) return "workspace:*";
|
|
520
520
|
if (spec === "catalog:" || spec === "catalog:default") {
|
|
521
521
|
const version$1 = meta.catalog[name];
|
|
522
|
-
if (!version$1) throw new Error(`catalog에 ${name} 버전이
|
|
522
|
+
if (!version$1) throw new Error(`catalog에 ${name} 버전이 없습니다`);
|
|
523
523
|
return version$1;
|
|
524
524
|
}
|
|
525
525
|
if (spec.startsWith("workspace:")) {
|
|
526
526
|
const version$1 = meta.versions[name];
|
|
527
|
-
if (!version$1) throw new Error(`${name}의 배포 버전을
|
|
527
|
+
if (!version$1) throw new Error(`${name}의 배포 버전을 모르겠습니다`);
|
|
528
528
|
return `^${version$1}`;
|
|
529
529
|
}
|
|
530
530
|
return spec;
|
|
@@ -598,7 +598,7 @@ function resolveRoot() {
|
|
|
598
598
|
for (const candidate of CANDIDATES) try {
|
|
599
599
|
if (statSync(candidate).isDirectory()) return candidate;
|
|
600
600
|
} catch {}
|
|
601
|
-
throw new Error("스토어프론트 템플릿을 찾지
|
|
601
|
+
throw new Error("스토어프론트 템플릿을 찾지 못했습니다");
|
|
602
602
|
}
|
|
603
603
|
const TEMPLATE_ROOT = resolveRoot();
|
|
604
604
|
function walk(dir, acc) {
|
|
@@ -623,7 +623,7 @@ function listTemplateFiles() {
|
|
|
623
623
|
*/
|
|
624
624
|
function readTemplateFile(path, options = {}) {
|
|
625
625
|
const normalized = path.replace(/^\.?\//, "");
|
|
626
|
-
if (!listTemplateFiles().includes(normalized)) throw new Error(`템플릿에 없는
|
|
626
|
+
if (!listTemplateFiles().includes(normalized)) throw new Error(`템플릿에 없는 파일입니다: ${path}`);
|
|
627
627
|
let file = join(TEMPLATE_ROOT, normalized);
|
|
628
628
|
if (normalized.endsWith(".gitignore") && !existsSync(file)) file = file.replace(/\.gitignore$/, BUNDLED_GITIGNORE);
|
|
629
629
|
const raw = readFileSync(file, "utf8");
|
|
@@ -720,9 +720,9 @@ const RULES = [
|
|
|
720
720
|
},
|
|
721
721
|
{
|
|
722
722
|
id: "checkout-origin",
|
|
723
|
-
title: "복귀 주소는 배포된 주소(요청 origin)에서
|
|
724
|
-
why: "결제
|
|
725
|
-
fix: "서버 함수에서 `getRequestUrl()`의 origin으로 복귀 주소를 만든다.
|
|
723
|
+
title: "복귀 주소는 코드에 고정하지 않고 배포된 주소(요청 origin)에서 만든다",
|
|
724
|
+
why: "주소를 코드에 고정하면 로컬·스테이징·운영 중 한 곳에서만 결제가 제대로 돌아온다. 셀러 콘솔 설정 › 결제의 결제 도메인은 선택형 허용 목록이다. 비워 두면 https 복귀 주소를 모두 받고, 등록하면 목록 밖의 복귀 주소를 서버가 거절한다",
|
|
725
|
+
fix: "서버 함수에서 `getRequestUrl()`의 origin으로 복귀 주소를 만든다. 결제 도메인을 등록해 두었다면 배포 도메인도 목록에 넣는다(테스트 결제는 localhost가 항상 허용된다)",
|
|
726
726
|
example: "const returnUrl = new URL(PAYMENT_RETURN_PATH, new URL(getRequestUrl()).origin);\nawait api.checkout.startPayment(checkoutId, { option, returnUrl: returnUrl.toString() });",
|
|
727
727
|
doc: "/guides/payment-settings",
|
|
728
728
|
templateFile: "src/routes/checkout.index.tsx"
|
|
@@ -852,7 +852,7 @@ function verifySources(files) {
|
|
|
852
852
|
if (opensPayment && !has("prepareWindow(")) inFile("payment-popup-prepare", opensPayment, "받아 둔 결제 시작 값으로 결제창을 열지만 클릭 시점에 `payments.prepareWindow()`로 창을 먼저 여는 코드가 없다", /\bpayments\.open\(|\.open\(\s*(start|paymentStart)\b/);
|
|
853
853
|
if (opensPayment && !/amounts/.test(opensPayment.content)) inFile("payment-amounts-final", opensPayment, "결제창을 열지만 결제 시작 응답의 `amounts`(확정 금액)를 화면에 반영하지 않는다", /\bpayments\.open\(/);
|
|
854
854
|
const fixedReturnUrl = matches(/returnUrl\s*[:=]\s*["'`]https?:\/\//);
|
|
855
|
-
if (fixedReturnUrl) inFile("checkout-origin", fixedReturnUrl, "복귀 주소를 고정 문자열로 두었다. 배포된 주소(요청 origin)에서
|
|
855
|
+
if (fixedReturnUrl) inFile("checkout-origin", fixedReturnUrl, "복귀 주소를 고정 문자열로 두었다. 배포된 주소(요청 origin)에서 만든다", /returnUrl\s*[:=]\s*["'`]https?:\/\//);
|
|
856
856
|
if (has("startPayment(") && !has(".result(")) inProject("payment-return-route", "결제 복귀 화면에서 `payments.result()`를 부르는 코드가 없다");
|
|
857
857
|
if (has(".result(") && !has("PROCESSING")) inProject("payment-processing-status", "결과 확인 중(`PROCESSING`)일 때 결제 상태를 다시 조회하는 코드가 없다");
|
|
858
858
|
if (has("startPayment(") && has("zipCode") && !has("quoteDelivery(")) inProject("delivery-quote-preview", "배송지 우편번호를 받지만 `checkout.quoteDelivery`로 배송비를 다시 계산하는 코드가 없다");
|
|
@@ -922,7 +922,7 @@ function buildReport(files) {
|
|
|
922
922
|
},
|
|
923
923
|
results,
|
|
924
924
|
findings: reported,
|
|
925
|
-
message: findings.length ? `규칙 ${RULES.length}개 중 ${violated}개를
|
|
925
|
+
message: findings.length ? `규칙 ${RULES.length}개 중 ${violated}개를 어겼습니다(위반 ${findings.length}건). 아래 findings의 fix대로 고친 뒤 다시 검사해주십시오` : `규칙 ${RULES.length}개를 모두 지켰습니다. 정적 검사가 잡지 못하는 것이 있으니 직접 띄워 결제까지 한 번 해보십시오`,
|
|
926
926
|
nextSteps: nextSteps(reported)
|
|
927
927
|
};
|
|
928
928
|
}
|
|
@@ -995,7 +995,7 @@ async function runCreate(argv) {
|
|
|
995
995
|
}
|
|
996
996
|
const target = resolve(args.dir);
|
|
997
997
|
if (existsSync(target) && readdirSync(target).length > 0 && !args.force) {
|
|
998
|
-
console.error(`폴더가 비어 있지
|
|
998
|
+
console.error(`폴더가 비어 있지 않습니다: ${target} (--force로 덮어쓸 수 있습니다)`);
|
|
999
999
|
return 1;
|
|
1000
1000
|
}
|
|
1001
1001
|
const name = args.name ?? basename(target);
|
|
@@ -1015,7 +1015,7 @@ async function runCreate(argv) {
|
|
|
1015
1015
|
const env = `SAYREN_API_URL=${store.storefrontApiBaseUrl}\nSAYREN_STORE_CODE=${store.storeCode}\n`;
|
|
1016
1016
|
writeFileSync(join(target, ".env"), env);
|
|
1017
1017
|
console.log(`.env 작성 — ${store.name} (${store.storeCode})`);
|
|
1018
|
-
} else console.log("SAYREN_TOKEN이 없어 .env는 비워
|
|
1018
|
+
} else console.log("SAYREN_TOKEN이 없어 .env는 비워 뒀습니다. SAYREN_API_URL과 SAYREN_STORE_CODE를 넣어주십시오");
|
|
1019
1019
|
const startedAt = Date.now();
|
|
1020
1020
|
const report$1 = buildReport(await collectSources(target));
|
|
1021
1021
|
const durationMs = Date.now() - startedAt;
|
|
@@ -1026,7 +1026,7 @@ async function runCreate(argv) {
|
|
|
1026
1026
|
}
|
|
1027
1027
|
console.log(`\n다음:\n cd ${args.dir}\n pnpm install\n pnpm dev`);
|
|
1028
1028
|
console.log(`
|
|
1029
|
-
디자인을 고친 뒤에는 MCP 도구 \`verify_storefront\`로 다시
|
|
1029
|
+
디자인을 고친 뒤에는 MCP 도구 \`verify_storefront\`로 다시 검사해주십시오. 결제·구매자 인증·방문 분석 규칙 ${report$1.summary.rules}개를 보고, 어긴 항목마다 고칠 자리와 수정 방법·예시 코드를 알려 줍니다.\n결제 도메인(셀러 콘솔 설정 › 결제)을 비워 두면 https 복귀 주소를 모두 받습니다. 도메인을 등록했다면 이 쇼핑몰을 배포한 주소도 목록에 넣어주십시오(테스트 결제는 localhost가 항상 허용됩니다).`);
|
|
1030
1030
|
if (config$1) await new Telemetry(config$1).reportVerify("create", report$1, durationMs);
|
|
1031
1031
|
return 0;
|
|
1032
1032
|
}
|
|
@@ -1276,7 +1276,7 @@ server.registerTool("verify_storefront", {
|
|
|
1276
1276
|
const files = await collectSources(projectDir);
|
|
1277
1277
|
if (!files.length) return text({
|
|
1278
1278
|
ok: false,
|
|
1279
|
-
message: `소스를 찾지
|
|
1279
|
+
message: `소스를 찾지 못했습니다: ${projectDir}`,
|
|
1280
1280
|
nextSteps: ["`projectDir`이 프로젝트 루트(package.json이 있는 폴더)의 절대 경로인지 확인한다.", "템플릿을 아직 받지 않았으면 `list_template_files`·`get_template_file`로 먼저 만든다."]
|
|
1281
1281
|
});
|
|
1282
1282
|
const report$1 = buildReport(files);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sayren/mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sayren-mcp": "./dist/index.mjs"
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
14
14
|
"zod": "^4.6.5",
|
|
15
|
-
"@sayren/
|
|
16
|
-
"@sayren/
|
|
15
|
+
"@sayren/storefront-sdk": "^0.12.0",
|
|
16
|
+
"@sayren/store-sdk": "^0.15.1"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@biomejs/biome": "^2.5.14",
|
package/template/README.md
CHANGED
|
@@ -74,8 +74,9 @@ EU 등 분석 쿠키에 사전 동의가 필요한 지역의 구매자를 받는
|
|
|
74
74
|
- `src/lib/session.server.ts`·`src/start.ts` — 구매자 세션 쿠키와 갱신이다. 갱신은 요청마다 한 번이고, 같은
|
|
75
75
|
리프레시 토큰의 동시 갱신은 한 번으로 모은다. 리프레시 토큰은 한 번 쓰면 폐기되므로 각자 갱신하면 로그아웃된다.
|
|
76
76
|
- loader가 서버 함수로 데이터를 받아 그리는 구조 — 컴포넌트에서 다시 받으면 검색 노출과 첫 화면이 죽는다.
|
|
77
|
-
- 결제 도메인 —
|
|
78
|
-
|
|
77
|
+
- 결제 도메인 — 셀러 콘솔 설정 › 결제의 **결제 도메인**은 선택형 허용 목록이다. 비워 두면 https 복귀 주소를 모두 받고,
|
|
78
|
+
등록하면 그 도메인에서만 결제가 된다. 등록했다면 이 쇼핑몰을 배포한 주소도 넣는다. 배포 주소는 PG(토스페이먼츠·포트원)
|
|
79
|
+
가맹 정보에도 등록한다. 테스트 결제는 localhost에서도 된다.
|
|
79
80
|
|
|
80
81
|
## 주의
|
|
81
82
|
|
|
@@ -18,6 +18,6 @@ export function resolveStoreCode(): string {
|
|
|
18
18
|
const fromHost = storeCodeFromHost(getRequestHeader("host"));
|
|
19
19
|
if (fromHost) return fromHost;
|
|
20
20
|
throw new Error(
|
|
21
|
-
"스토어 코드를 알 수
|
|
21
|
+
"스토어 코드를 알 수 없습니다. SAYREN_STORE_CODE 환경변수를 설정하거나 서브도메인으로 서비스해주십시오",
|
|
22
22
|
);
|
|
23
23
|
}
|
|
@@ -11,7 +11,8 @@ import type { PublicConfig } from "./config";
|
|
|
11
11
|
* 3. 모바일이나 팝업 차단이면 같은 탭이 결제 서비스로 이동하고, 결제가 끝나면 복귀 화면(`/checkout/return`)으로
|
|
12
12
|
* 돌아온다. 복귀 화면은 `payments.result()` 한 줄로 결과를 읽는다.
|
|
13
13
|
* 4. 결과를 모르면(`PROCESSING`) 실패로 단정하지 말고 계속 조회한다 — 다시 결제하게 하면 이중 결제가 난다.
|
|
14
|
-
* 5.
|
|
14
|
+
* 5. 셀러 콘솔 설정 › 결제의 결제 도메인을 등록했다면 복귀 주소의 도메인이 그 안에 있어야 한다(비우면 https 모두 허용,
|
|
15
|
+
* 테스트 결제는 localhost 허용).
|
|
15
16
|
*/
|
|
16
17
|
/**
|
|
17
18
|
* 설정마다 하나만 만든다. `prepareWindow()`로 연 창과 그 결과를 기다리는 감시가 같은 인스턴스에 묶여 있어서,
|
|
@@ -67,14 +67,14 @@ function Root() {
|
|
|
67
67
|
function RootError({ error }: ErrorComponentProps) {
|
|
68
68
|
return (
|
|
69
69
|
<ErrorView
|
|
70
|
-
title="문제가
|
|
70
|
+
title="문제가 생겼습니다"
|
|
71
71
|
detail={error instanceof Error ? error.message : undefined}
|
|
72
72
|
/>
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function NotFound() {
|
|
77
|
-
return <ErrorView title="찾을 수
|
|
77
|
+
return <ErrorView title="찾을 수 없습니다" />;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function ErrorView({ title, detail }: { title: string; detail?: string }) {
|
|
@@ -53,7 +53,7 @@ const changeConsents = createServerFn({ method: "POST" })
|
|
|
53
53
|
.validator(consentInput)
|
|
54
54
|
.handler(async ({ data }) => {
|
|
55
55
|
const accessToken = readToken();
|
|
56
|
-
if (!accessToken) return { error: "다시 로그인해
|
|
56
|
+
if (!accessToken) return { error: "다시 로그인해 주십시오" };
|
|
57
57
|
try {
|
|
58
58
|
await apiFor({ accessToken }).member.updateConsents({
|
|
59
59
|
...(data.reconsent ? { terms: true as const, privacy: true as const } : {}),
|
|
@@ -73,7 +73,7 @@ const startLink = createServerFn({ method: "POST" })
|
|
|
73
73
|
.validator(providerInput)
|
|
74
74
|
.handler(async ({ data }) => {
|
|
75
75
|
const accessToken = readToken();
|
|
76
|
-
if (!accessToken) return { url: null, error: "다시 로그인해
|
|
76
|
+
if (!accessToken) return { url: null, error: "다시 로그인해 주십시오" };
|
|
77
77
|
const origin = new URL(getRequestUrl()).origin;
|
|
78
78
|
try {
|
|
79
79
|
const started = await authFor().linkIdp(accessToken, data.provider, {
|
|
@@ -95,7 +95,7 @@ const unlink = createServerFn({ method: "POST" })
|
|
|
95
95
|
.validator(providerInput)
|
|
96
96
|
.handler(async ({ data }) => {
|
|
97
97
|
const accessToken = readToken();
|
|
98
|
-
if (!accessToken) return { error: "다시 로그인해
|
|
98
|
+
if (!accessToken) return { error: "다시 로그인해 주십시오" };
|
|
99
99
|
try {
|
|
100
100
|
await authFor().unlinkIdp(accessToken, data.provider);
|
|
101
101
|
return { error: null };
|
|
@@ -105,18 +105,18 @@ const unlink = createServerFn({ method: "POST" })
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
const ERROR_MESSAGE: Record<string, string> = {
|
|
108
|
-
IDENTITY_IN_USE: "이 계정은 이미 다른 회원에 연결돼
|
|
108
|
+
IDENTITY_IN_USE: "이 계정은 이미 다른 회원에 연결돼 있습니다",
|
|
109
109
|
PROVIDER_ALREADY_LINKED:
|
|
110
|
-
"같은 서비스의 다른 계정이 이미 연결돼
|
|
111
|
-
LAST_LOGIN_METHOD: "마지막 로그인 수단은 해제할 수
|
|
112
|
-
access_denied: "연결을
|
|
113
|
-
state_mismatch: "연결 요청이
|
|
110
|
+
"같은 서비스의 다른 계정이 이미 연결돼 있습니다. 해제한 뒤 다시 연결해 주십시오",
|
|
111
|
+
LAST_LOGIN_METHOD: "마지막 로그인 수단은 해제할 수 없습니다. 다른 계정을 먼저 연결해 주십시오",
|
|
112
|
+
access_denied: "연결을 취소했습니다",
|
|
113
|
+
state_mismatch: "연결 요청이 만료됐습니다. 다시 시도해 주십시오",
|
|
114
114
|
};
|
|
115
115
|
|
|
116
116
|
function accountErrorMessage(error: unknown): string {
|
|
117
117
|
if (error instanceof ApiError)
|
|
118
|
-
return ERROR_MESSAGE[error.code] ?? "처리하지
|
|
119
|
-
return "처리하지
|
|
118
|
+
return ERROR_MESSAGE[error.code] ?? "처리하지 못했습니다. 잠시 후 다시 시도해 주십시오";
|
|
119
|
+
return "처리하지 못했습니다. 잠시 후 다시 시도해 주십시오";
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
export const Route = createFileRoute("/account")({
|
|
@@ -135,7 +135,7 @@ function Account() {
|
|
|
135
135
|
const error =
|
|
136
136
|
actionError ??
|
|
137
137
|
(callbackError
|
|
138
|
-
? (ERROR_MESSAGE[callbackError] ?? "연결하지
|
|
138
|
+
? (ERROR_MESSAGE[callbackError] ?? "연결하지 못했습니다. 다시 시도해 주십시오")
|
|
139
139
|
: null);
|
|
140
140
|
|
|
141
141
|
const changeConsent = (data: { reconsent?: boolean; marketing?: boolean }) => {
|
|
@@ -187,7 +187,7 @@ function Account() {
|
|
|
187
187
|
<h2 className="font-bold text-lg">로그인 수단</h2>
|
|
188
188
|
{linked ? (
|
|
189
189
|
<p className="rounded-md bg-chip px-3 py-2 text-sm">
|
|
190
|
-
{PROVIDER_NAME[linked]} 계정을
|
|
190
|
+
{PROVIDER_NAME[linked]} 계정을 연결했습니다
|
|
191
191
|
</p>
|
|
192
192
|
) : null}
|
|
193
193
|
<form onSubmit={submit}>
|
|
@@ -240,7 +240,7 @@ function Account() {
|
|
|
240
240
|
</form>
|
|
241
241
|
{error ? <p className="text-point text-sm">{error}</p> : null}
|
|
242
242
|
<p className="text-muted text-xs">
|
|
243
|
-
연결한 계정으로도 같은 회원으로
|
|
243
|
+
연결한 계정으로도 같은 회원으로 로그인합니다. 마지막 로그인 수단은 해제할 수 없습니다.
|
|
244
244
|
</p>
|
|
245
245
|
</section>
|
|
246
246
|
|
|
@@ -252,7 +252,7 @@ function Account() {
|
|
|
252
252
|
<span className="font-medium">이용약관 · 개인정보 수집이용</span>
|
|
253
253
|
<span className="text-muted text-xs">
|
|
254
254
|
{consents.reconsentRequired
|
|
255
|
-
? "약관이
|
|
255
|
+
? "약관이 개정됐습니다. 다시 동의해 주십시오"
|
|
256
256
|
: `동의 완료 · v${consents.terms.agreedVersion ?? "-"}`}
|
|
257
257
|
</span>
|
|
258
258
|
{/* 스토어가 약관 주소를 정했으면 열어 볼 수 있게 한다 */}
|
|
@@ -73,7 +73,7 @@ function CartPage() {
|
|
|
73
73
|
if (!cart.items.length) {
|
|
74
74
|
return (
|
|
75
75
|
<div className="space-y-4 py-16 text-center">
|
|
76
|
-
<p className="text-muted">장바구니가
|
|
76
|
+
<p className="text-muted">장바구니가 비었습니다</p>
|
|
77
77
|
<Link to="/products" className="inline-block rounded-md bg-ink px-5 py-3 text-white">
|
|
78
78
|
상품 보러 가기
|
|
79
79
|
</Link>
|
|
@@ -107,7 +107,7 @@ function CartPage() {
|
|
|
107
107
|
) : null}
|
|
108
108
|
{!item.purchasable ? (
|
|
109
109
|
<span className="text-point text-xs">
|
|
110
|
-
{item.unpurchasableReason === "SOLD_OUT" ? "
|
|
110
|
+
{item.unpurchasableReason === "SOLD_OUT" ? "품절입니다" : "지금은 살 수 없습니다"}
|
|
111
111
|
</span>
|
|
112
112
|
) : null}
|
|
113
113
|
<div className="flex items-center gap-3 pt-1">
|
|
@@ -171,7 +171,7 @@ function CartPage() {
|
|
|
171
171
|
{loginRequired ? (
|
|
172
172
|
<div className="flex w-full flex-col gap-2 md:w-64">
|
|
173
173
|
<p className="text-center text-muted text-xs">
|
|
174
|
-
이 스토어는 회원만 주문할 수
|
|
174
|
+
이 스토어는 회원만 주문할 수 있습니다. 담은 상품은 로그인하면 그대로 이어집니다
|
|
175
175
|
</p>
|
|
176
176
|
<Link
|
|
177
177
|
to="/login"
|
|
@@ -34,11 +34,11 @@ function CheckoutComplete() {
|
|
|
34
34
|
return (
|
|
35
35
|
<div className="space-y-6 py-10 text-center">
|
|
36
36
|
<div className="space-y-2">
|
|
37
|
-
<h1 className="font-bold text-2xl">주문을
|
|
37
|
+
<h1 className="font-bold text-2xl">주문을 완료했습니다</h1>
|
|
38
38
|
{orderId ? (
|
|
39
39
|
<p className="text-muted text-sm">주문번호 {orderId}</p>
|
|
40
40
|
) : (
|
|
41
|
-
<p className="text-muted text-sm">주문 내역에서 주문을
|
|
41
|
+
<p className="text-muted text-sm">주문 내역에서 주문을 확인해주십시오</p>
|
|
42
42
|
)}
|
|
43
43
|
</div>
|
|
44
44
|
|
|
@@ -64,7 +64,10 @@ const quoteDelivery = createServerFn({ method: "POST" })
|
|
|
64
64
|
const quote = await api.checkout.quoteDelivery(data.checkoutId, { zipCode: data.zipCode });
|
|
65
65
|
return { quote, error: null };
|
|
66
66
|
} catch {
|
|
67
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
quote: null,
|
|
69
|
+
error: "배송비를 확인하지 못했습니다. 우편번호를 다시 확인해주십시오.",
|
|
70
|
+
};
|
|
68
71
|
}
|
|
69
72
|
});
|
|
70
73
|
|
|
@@ -92,7 +95,7 @@ const startPayment = createServerFn({ method: "POST" })
|
|
|
92
95
|
.handler(async ({ data }) => {
|
|
93
96
|
const accessToken = readToken();
|
|
94
97
|
const api = apiFor({ accessToken, cartToken: readCartToken() });
|
|
95
|
-
// 복귀 주소 —
|
|
98
|
+
// 복귀 주소 — 결제 도메인을 등록한 스토어는 이 도메인이 목록 안이어야 한다(테스트 결제는 localhost 허용)
|
|
96
99
|
const returnUrl = new URL(PAYMENT_RETURN_PATH, new URL(getRequestUrl()).origin);
|
|
97
100
|
returnUrl.searchParams.set("back", data.back);
|
|
98
101
|
try {
|
|
@@ -125,20 +128,21 @@ const startPayment = createServerFn({ method: "POST" })
|
|
|
125
128
|
});
|
|
126
129
|
|
|
127
130
|
function startErrorMessage(error: unknown): string {
|
|
128
|
-
if (!(error instanceof ApiError))
|
|
131
|
+
if (!(error instanceof ApiError))
|
|
132
|
+
return "결제를 시작하지 못했습니다. 잠시 후 다시 시도해주십시오.";
|
|
129
133
|
switch (error.code) {
|
|
130
134
|
case "PAYMENT_OPTION_UNAVAILABLE":
|
|
131
|
-
return "지금 쓸 수 없는
|
|
135
|
+
return "지금 쓸 수 없는 결제수단입니다. 다른 결제수단을 골라주십시오.";
|
|
132
136
|
case "PAYMENT_PROVIDER_UNAVAILABLE":
|
|
133
|
-
return "결제창을 준비하지
|
|
137
|
+
return "결제창을 준비하지 못했습니다. 다른 결제수단으로 시도해주십시오.";
|
|
134
138
|
case "PAYMENT_NOT_CONFIGURED":
|
|
135
|
-
return "결제 수단을 준비하고
|
|
139
|
+
return "결제 수단을 준비하고 있습니다. 잠시 후 다시 시도해주십시오.";
|
|
136
140
|
case "CASH_RECEIPT_NOT_AVAILABLE":
|
|
137
|
-
return "현금영수증은 계좌이체·가상계좌 결제에서만 신청할 수
|
|
141
|
+
return "현금영수증은 계좌이체·가상계좌 결제에서만 신청할 수 있습니다.";
|
|
138
142
|
case "RETURN_URL_NOT_ALLOWED":
|
|
139
|
-
return "이 쇼핑몰 주소에서는 결제할 수
|
|
143
|
+
return "이 쇼핑몰 주소에서는 결제할 수 없습니다. 셀러 콘솔 설정 › 결제에서 결제 도메인을 확인해주십시오.";
|
|
140
144
|
default:
|
|
141
|
-
return "결제를 시작하지
|
|
145
|
+
return "결제를 시작하지 못했습니다. 입력한 내용을 확인하고 다시 시도해주십시오.";
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -210,10 +214,10 @@ function Checkout() {
|
|
|
210
214
|
}
|
|
211
215
|
setNotice(
|
|
212
216
|
result.status === "CANCELED"
|
|
213
|
-
? "결제를
|
|
217
|
+
? "결제를 취소했습니다. 다른 결제수단으로 다시 시도해주십시오."
|
|
214
218
|
: result.status === "FAILED"
|
|
215
219
|
? result.message
|
|
216
|
-
: "결제 결과를 확인하지
|
|
220
|
+
: "결제 결과를 확인하지 못했습니다. 주문 내역을 확인해주십시오.",
|
|
217
221
|
);
|
|
218
222
|
};
|
|
219
223
|
|
|
@@ -221,7 +225,7 @@ function Checkout() {
|
|
|
221
225
|
event.preventDefault();
|
|
222
226
|
const option = options.find((item) => paymentOptionKey(item) === selected);
|
|
223
227
|
if (!option) {
|
|
224
|
-
setNotice("결제수단을
|
|
228
|
+
setNotice("결제수단을 골라주십시오.");
|
|
225
229
|
return;
|
|
226
230
|
}
|
|
227
231
|
const form = new FormData(event.currentTarget);
|
|
@@ -233,7 +237,7 @@ function Checkout() {
|
|
|
233
237
|
identityNumber: field("cashReceiptNumber"),
|
|
234
238
|
});
|
|
235
239
|
if (!parsed.success) {
|
|
236
|
-
setNotice(parsed.error.issues[0]?.message ?? "현금영수증 번호를
|
|
240
|
+
setNotice(parsed.error.issues[0]?.message ?? "현금영수증 번호를 확인해주십시오.");
|
|
237
241
|
return;
|
|
238
242
|
}
|
|
239
243
|
cashReceipt = parsed.data;
|
|
@@ -275,7 +279,7 @@ function Checkout() {
|
|
|
275
279
|
})
|
|
276
280
|
.catch(() => {
|
|
277
281
|
paymentWindow.close();
|
|
278
|
-
setNotice("결제를 시작하지
|
|
282
|
+
setNotice("결제를 시작하지 못했습니다. 잠시 후 다시 시도해주십시오.");
|
|
279
283
|
})
|
|
280
284
|
.finally(() => setSubmitting(false));
|
|
281
285
|
};
|
|
@@ -293,7 +297,7 @@ function Checkout() {
|
|
|
293
297
|
placeholder="04524"
|
|
294
298
|
required
|
|
295
299
|
onBlur={handleZipBlur}
|
|
296
|
-
hint="입력하면 지역 추가 배송비까지 반영한 배송비를 보여
|
|
300
|
+
hint="입력하면 지역 추가 배송비까지 반영한 배송비를 보여 줍니다"
|
|
297
301
|
/>
|
|
298
302
|
<Field label="주소" name="address1" required />
|
|
299
303
|
<Field label="상세 주소" name="address2" />
|
|
@@ -310,7 +314,7 @@ function Checkout() {
|
|
|
310
314
|
name="orderPassword"
|
|
311
315
|
type="password"
|
|
312
316
|
required
|
|
313
|
-
hint="주문 내역을 찾을 때 쓸 6자 이상
|
|
317
|
+
hint="주문 내역을 찾을 때 쓸 6자 이상 비밀번호입니다"
|
|
314
318
|
/>
|
|
315
319
|
</section>
|
|
316
320
|
)}
|
|
@@ -318,7 +322,7 @@ function Checkout() {
|
|
|
318
322
|
<fieldset className="space-y-2">
|
|
319
323
|
<legend className="mb-3 font-bold text-lg">결제수단</legend>
|
|
320
324
|
{options.length === 0 ? (
|
|
321
|
-
<p className="text-muted text-sm">지금 결제할 수 있는 결제수단이
|
|
325
|
+
<p className="text-muted text-sm">지금 결제할 수 있는 결제수단이 없습니다.</p>
|
|
322
326
|
) : (
|
|
323
327
|
<div className="grid grid-cols-2 gap-2">
|
|
324
328
|
{options.map((option) => {
|
|
@@ -438,7 +442,7 @@ function Checkout() {
|
|
|
438
442
|
))}
|
|
439
443
|
{checkout.testPayment ? (
|
|
440
444
|
<p className="rounded-md bg-chip px-3 py-2 text-xs">
|
|
441
|
-
테스트
|
|
445
|
+
테스트 결제입니다. 실제로 돈이 오가지 않습니다.
|
|
442
446
|
</p>
|
|
443
447
|
) : null}
|
|
444
448
|
{notice ? <p className="rounded-md bg-chip px-3 py-2 text-sm">{notice}</p> : null}
|
|
@@ -451,10 +455,10 @@ function Checkout() {
|
|
|
451
455
|
function deliveryNotes(delivery: CheckoutDelivery): string[] {
|
|
452
456
|
const notes: string[] = [];
|
|
453
457
|
if (delivery.bundleCount >= 2) {
|
|
454
|
-
notes.push(`배송 묶음 ${delivery.bundleCount}건 — 묶음마다 배송비가
|
|
458
|
+
notes.push(`배송 묶음 ${delivery.bundleCount}건 — 묶음마다 배송비가 붙습니다.`);
|
|
455
459
|
}
|
|
456
|
-
if (delivery.freeByThreshold) notes.push("무료배송 기준 금액을 넘겨 기본 배송비가
|
|
457
|
-
if (delivery.zipCode === null) notes.push("배송지를 입력하면 배송비가
|
|
460
|
+
if (delivery.freeByThreshold) notes.push("무료배송 기준 금액을 넘겨 기본 배송비가 빠졌습니다.");
|
|
461
|
+
if (delivery.zipCode === null) notes.push("배송지를 입력하면 배송비가 확정됩니다.");
|
|
458
462
|
return notes;
|
|
459
463
|
}
|
|
460
464
|
|
|
@@ -107,17 +107,17 @@ function PaymentReturn() {
|
|
|
107
107
|
<div className="mx-auto max-w-md space-y-6 py-16 text-center">
|
|
108
108
|
{view.kind === "working" || view.kind === "processing" ? (
|
|
109
109
|
<>
|
|
110
|
-
<h1 className="font-bold text-2xl">결제를 확인하고
|
|
110
|
+
<h1 className="font-bold text-2xl">결제를 확인하고 있습니다</h1>
|
|
111
111
|
<p className="text-muted text-sm">
|
|
112
112
|
{view.kind === "processing"
|
|
113
|
-
? "결제사에서 결과를 확인하는
|
|
114
|
-
: "잠시만
|
|
113
|
+
? "결제사에서 결과를 확인하는 중입니다. 이 화면을 닫지 말고 잠시만 기다려주십시오."
|
|
114
|
+
: "잠시만 기다려주십시오."}
|
|
115
115
|
</p>
|
|
116
116
|
</>
|
|
117
117
|
) : view.kind === "failed" ? (
|
|
118
118
|
<>
|
|
119
119
|
<h1 className="font-bold text-2xl">
|
|
120
|
-
{view.canceled ? "결제를
|
|
120
|
+
{view.canceled ? "결제를 취소했습니다" : "결제하지 못했습니다"}
|
|
121
121
|
</h1>
|
|
122
122
|
{view.canceled ? null : <p className="text-muted text-sm">{view.message}</p>}
|
|
123
123
|
<div className="flex justify-center gap-2">
|
|
@@ -128,10 +128,10 @@ function PaymentReturn() {
|
|
|
128
128
|
</>
|
|
129
129
|
) : (
|
|
130
130
|
<>
|
|
131
|
-
<h1 className="font-bold text-2xl">결제 결과를 확인하지
|
|
131
|
+
<h1 className="font-bold text-2xl">결제 결과를 확인하지 못했습니다</h1>
|
|
132
132
|
<p className="text-muted text-sm">
|
|
133
|
-
결제를 마쳤다면 잠시 뒤 주문 내역에
|
|
134
|
-
|
|
133
|
+
결제를 마쳤다면 잠시 뒤 주문 내역에 나타납니다. 다시 결제하기 전에 주문 내역을 먼저
|
|
134
|
+
확인해주십시오.
|
|
135
135
|
</p>
|
|
136
136
|
<Link to="/orders" className="inline-block rounded-md border border-ink px-5 py-3">
|
|
137
137
|
주문 내역
|
|
@@ -48,7 +48,7 @@ function Home() {
|
|
|
48
48
|
) : null}
|
|
49
49
|
|
|
50
50
|
<ProductSection listId="home:recommend" title="추천 상품" products={recommended} />
|
|
51
|
-
<ProductSection listId="home:latest" title="새로
|
|
51
|
+
<ProductSection listId="home:latest" title="새로 들어온 상품" products={latest} />
|
|
52
52
|
</div>
|
|
53
53
|
);
|
|
54
54
|
}
|
|
@@ -74,18 +74,18 @@ const passwordLogin = createServerFn({ method: "POST" })
|
|
|
74
74
|
|
|
75
75
|
function loginErrorMessage(error: unknown): string {
|
|
76
76
|
if (error instanceof ApiError) {
|
|
77
|
-
if (error.code === "INVALID_CREDENTIALS") return "이메일이나 비밀번호가 맞지
|
|
78
|
-
if (error.code === "PASSWORD_LOGIN_DISABLED") return "이 스토어는 소셜 로그인만
|
|
77
|
+
if (error.code === "INVALID_CREDENTIALS") return "이메일이나 비밀번호가 맞지 않습니다";
|
|
78
|
+
if (error.code === "PASSWORD_LOGIN_DISABLED") return "이 스토어는 소셜 로그인만 받습니다";
|
|
79
79
|
if (error.code === "ACCOUNT_LOCKED")
|
|
80
|
-
return "로그인을 여러 번 실패해 잠시
|
|
81
|
-
if (error.status === 400) return "이메일과 비밀번호를 확인해
|
|
80
|
+
return "로그인을 여러 번 실패해 잠시 잠겼습니다. 10분 뒤에 다시 시도해 주십시오";
|
|
81
|
+
if (error.status === 400) return "이메일과 비밀번호를 확인해 주십시오";
|
|
82
82
|
}
|
|
83
|
-
return "로그인하지
|
|
83
|
+
return "로그인하지 못했습니다. 잠시 후 다시 시도해 주십시오";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const CALLBACK_ERRORS: Record<string, string> = {
|
|
87
|
-
access_denied: "로그인을
|
|
88
|
-
state_mismatch: "로그인 요청이
|
|
87
|
+
access_denied: "로그인을 취소했습니다",
|
|
88
|
+
state_mismatch: "로그인 요청이 만료됐습니다. 다시 시도해 주십시오",
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
export const Route = createFileRoute("/login")({
|
|
@@ -105,7 +105,7 @@ function Login() {
|
|
|
105
105
|
const error =
|
|
106
106
|
actionError ??
|
|
107
107
|
(callbackError
|
|
108
|
-
? (CALLBACK_ERRORS[callbackError] ?? "로그인하지
|
|
108
|
+
? (CALLBACK_ERRORS[callbackError] ?? "로그인하지 못했습니다. 다시 시도해 주십시오")
|
|
109
109
|
: null);
|
|
110
110
|
|
|
111
111
|
const submitSocial = (event: React.FormEvent<HTMLFormElement>) => {
|
|
@@ -195,7 +195,7 @@ function Login() {
|
|
|
195
195
|
) : null}
|
|
196
196
|
{error ? <p className="text-point text-sm">{error}</p> : null}
|
|
197
197
|
{social.length ? (
|
|
198
|
-
<p className="text-center text-muted text-xs">처음 소셜 로그인하면 가입이 함께
|
|
198
|
+
<p className="text-center text-muted text-xs">처음 소셜 로그인하면 가입이 함께 됩니다</p>
|
|
199
199
|
) : null}
|
|
200
200
|
</div>
|
|
201
201
|
);
|
|
@@ -62,18 +62,18 @@ const createClaim = createServerFn({ method: "POST" })
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
function claimErrorMessage(error: unknown): string {
|
|
65
|
-
if (!(error instanceof ApiError)) return "접수하지
|
|
65
|
+
if (!(error instanceof ApiError)) return "접수하지 못했습니다. 잠시 후 다시 시도해주십시오.";
|
|
66
66
|
switch (error.code) {
|
|
67
67
|
case "INVALID_QUANTITY":
|
|
68
|
-
return "남은 수량보다 많이
|
|
68
|
+
return "남은 수량보다 많이 신청했습니다. 수량을 확인해주십시오.";
|
|
69
69
|
case "CLAIM_IN_PROGRESS":
|
|
70
|
-
return "이미 진행 중인 신청이
|
|
70
|
+
return "이미 진행 중인 신청이 있습니다.";
|
|
71
71
|
case "INVALID_STATUS":
|
|
72
|
-
return "지금은 신청할 수 없는
|
|
72
|
+
return "지금은 신청할 수 없는 상태입니다. 주문 상태를 확인해주십시오.";
|
|
73
73
|
case "PERIOD_EXPIRED":
|
|
74
|
-
return "반품·교환 가능 기간이
|
|
74
|
+
return "반품·교환 가능 기간이 지났습니다.";
|
|
75
75
|
default:
|
|
76
|
-
return error.message || "접수하지
|
|
76
|
+
return error.message || "접수하지 못했습니다. 잠시 후 다시 시도해주십시오.";
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -147,7 +147,7 @@ function OrderDetail() {
|
|
|
147
147
|
|
|
148
148
|
{result ? (
|
|
149
149
|
<div className="space-y-1 rounded-md bg-chip p-4 text-sm">
|
|
150
|
-
<p className="font-semibold">신청이
|
|
150
|
+
<p className="font-semibold">신청이 접수됐습니다 (수량 {result.quantity}개)</p>
|
|
151
151
|
{/* 상품 금액 환불은 수량 비율이고, 반품 배송비는 이미 빠져 있다. 배송비 환불은 따로 온다 */}
|
|
152
152
|
{result.expectedRefundAmount === null ? null : (
|
|
153
153
|
<p className="text-muted">
|
|
@@ -271,9 +271,9 @@ function ClaimForm({
|
|
|
271
271
|
const quantity = Number(quantityText);
|
|
272
272
|
const quantityError =
|
|
273
273
|
quantityText.trim() === "" || !Number.isInteger(quantity)
|
|
274
|
-
? "수량을
|
|
274
|
+
? "수량을 입력해주십시오."
|
|
275
275
|
: quantity < 1 || quantity > item.activeQuantity
|
|
276
|
-
? `수량은 1개 이상 ${item.activeQuantity}개 이하로
|
|
276
|
+
? `수량은 1개 이상 ${item.activeQuantity}개 이하로 입력해주십시오.`
|
|
277
277
|
: null;
|
|
278
278
|
return (
|
|
279
279
|
<form
|
|
@@ -107,7 +107,7 @@ function ProductDetail() {
|
|
|
107
107
|
setError(null);
|
|
108
108
|
addToCart({ data: { productId: product.productId, optionId, quantity } })
|
|
109
109
|
.then(() => navigate({ to: "/cart" }))
|
|
110
|
-
.catch(() => setError("장바구니에 담지
|
|
110
|
+
.catch(() => setError("장바구니에 담지 못했습니다. 다시 시도해주십시오"))
|
|
111
111
|
.finally(() => setSubmitting(false));
|
|
112
112
|
}}
|
|
113
113
|
>
|
|
@@ -71,7 +71,7 @@ function Products() {
|
|
|
71
71
|
type="search"
|
|
72
72
|
name="keyword"
|
|
73
73
|
defaultValue={applied.keyword ?? ""}
|
|
74
|
-
placeholder="
|
|
74
|
+
placeholder="상품 검색"
|
|
75
75
|
className="h-11 flex-1 rounded-md border border-line px-3"
|
|
76
76
|
/>
|
|
77
77
|
{applied.categoryId ? (
|
|
@@ -104,7 +104,7 @@ function Products() {
|
|
|
104
104
|
</div>
|
|
105
105
|
|
|
106
106
|
{page.contents.length === 0 ? (
|
|
107
|
-
<p className="py-16 text-center text-muted">조건에 맞는 상품이
|
|
107
|
+
<p className="py-16 text-center text-muted">조건에 맞는 상품이 없습니다</p>
|
|
108
108
|
) : (
|
|
109
109
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
110
110
|
{page.contents.map((product) => (
|