@nhnpayco-dev/mcp-devcenter 1.0.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/LICENSE +21 -0
- package/README.md +197 -0
- package/dist/data/api-specs.json +8135 -0
- package/dist/data/error-codes.json +2212 -0
- package/dist/data/guides/calculate.md +164 -0
- package/dist/data/guides/calculate.meta.json +22 -0
- package/dist/data/guides/membership-api.md +967 -0
- package/dist/data/guides/membership-api.meta.json +27 -0
- package/dist/data/guides/membership-check.md +30 -0
- package/dist/data/guides/membership-check.meta.json +24 -0
- package/dist/data/guides/membership-code.md +59 -0
- package/dist/data/guides/membership-code.meta.json +22 -0
- package/dist/data/guides/membership-sample.md +23 -0
- package/dist/data/guides/membership-sample.meta.json +22 -0
- package/dist/data/guides/membership-start.md +74 -0
- package/dist/data/guides/membership-start.meta.json +26 -0
- package/dist/data/guides/membership-vmem.md +512 -0
- package/dist/data/guides/membership-vmem.meta.json +24 -0
- package/dist/data/guides/offline-api.md +743 -0
- package/dist/data/guides/offline-api.meta.json +50 -0
- package/dist/data/guides/offline-authapi.md +1170 -0
- package/dist/data/guides/offline-authapi.meta.json +81 -0
- package/dist/data/guides/offline-check.md +33 -0
- package/dist/data/guides/offline-check.meta.json +24 -0
- package/dist/data/guides/offline-code.md +138 -0
- package/dist/data/guides/offline-code.meta.json +22 -0
- package/dist/data/guides/offline-sample.md +34 -0
- package/dist/data/guides/offline-sample.meta.json +22 -0
- package/dist/data/guides/offline-start.md +99 -0
- package/dist/data/guides/offline-start.meta.json +25 -0
- package/dist/data/guides/offline-vcat.md +370 -0
- package/dist/data/guides/offline-vcat.meta.json +35 -0
- package/dist/data/guides/online-autopay.md +495 -0
- package/dist/data/guides/online-autopay.meta.json +50 -0
- package/dist/data/guides/online-check.md +57 -0
- package/dist/data/guides/online-check.meta.json +24 -0
- package/dist/data/guides/online-code.md +367 -0
- package/dist/data/guides/online-code.meta.json +22 -0
- package/dist/data/guides/online-easypay.md +530 -0
- package/dist/data/guides/online-easypay.meta.json +50 -0
- package/dist/data/guides/online-etc.md +167 -0
- package/dist/data/guides/online-etc.meta.json +36 -0
- package/dist/data/guides/online-extra.md +190 -0
- package/dist/data/guides/online-extra.meta.json +35 -0
- package/dist/data/guides/online-extradata.md +103 -0
- package/dist/data/guides/online-extradata.meta.json +54 -0
- package/dist/data/guides/online-sample.md +23 -0
- package/dist/data/guides/online-sample.meta.json +22 -0
- package/dist/data/guides/online-start.md +122 -0
- package/dist/data/guides/online-start.meta.json +25 -0
- package/dist/data/guides/order-check.md +35 -0
- package/dist/data/guides/order-check.meta.json +24 -0
- package/dist/data/guides/order-code.md +143 -0
- package/dist/data/guides/order-code.meta.json +22 -0
- package/dist/data/guides/order-sample.md +31 -0
- package/dist/data/guides/order-sample.meta.json +22 -0
- package/dist/data/guides/order-start.md +94 -0
- package/dist/data/guides/order-start.meta.json +25 -0
- package/dist/data/guides/order-vorder.md +326 -0
- package/dist/data/guides/order-vorder.meta.json +24 -0
- package/dist/data/index.json +782 -0
- package/dist/data/sitemap.json +302 -0
- package/dist/data.js +42 -0
- package/dist/excel.js +320 -0
- package/dist/index.js +25 -0
- package/dist/tools.js +929 -0
- package/dist/validation-buffer.js +29 -0
- package/package.json +58 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
// MCP 도구 11개 등록.
|
|
2
|
+
// 외부 제휴사 LLM 사용자가 호출하는 도구 표면.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { pushRecord, getAll as getAllRecords, clearAll as clearAllRecords, size as bufferSize, extractOrderKeys, } from "./validation-buffer.js";
|
|
5
|
+
import { exportValidationToExcel } from "./excel.js";
|
|
6
|
+
// ---------- 공통 헬퍼 ----------
|
|
7
|
+
function text(t) {
|
|
8
|
+
return { content: [{ type: "text", text: t }] };
|
|
9
|
+
}
|
|
10
|
+
function notFound(msg) {
|
|
11
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
12
|
+
}
|
|
13
|
+
/** 본문 markdown 에서 모든 ```...``` 코드 블록 추출 */
|
|
14
|
+
function extractCodeBlocks(md, language) {
|
|
15
|
+
const re = /```([a-zA-Z0-9_-]*)\s*\n([\s\S]*?)```/g;
|
|
16
|
+
const out = [];
|
|
17
|
+
let m;
|
|
18
|
+
while ((m = re.exec(md)) !== null) {
|
|
19
|
+
const lang = m[1].toLowerCase();
|
|
20
|
+
if (language && lang && lang !== language.toLowerCase())
|
|
21
|
+
continue;
|
|
22
|
+
out.push(m[0]);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/** payload 의 nested 객체/배열을 `parent.field` 형식으로 평탄화.
|
|
27
|
+
*
|
|
28
|
+
* - 객체: 키를 점(.)으로 연결
|
|
29
|
+
* - 배열: 인덱스는 무시하고 같은 키로 모든 객체 평탄화 (각 객체가 동일 스키마라고 가정)
|
|
30
|
+
* - 같은 평탄화 키에 여러 값이 매핑될 수 있어 Map<string, unknown[]> 반환
|
|
31
|
+
* - 객체/배열 자체도 키로 등록 (예: `orderProducts` → 배열 값)
|
|
32
|
+
*
|
|
33
|
+
* 예) {orderProducts: [{cpId: "X"}]} → {
|
|
34
|
+
* "orderProducts": [[{cpId:"X"}]],
|
|
35
|
+
* "orderProducts.cpId": ["X"]
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
function flattenPayload(payload) {
|
|
39
|
+
const out = new Map();
|
|
40
|
+
const add = (k, v) => {
|
|
41
|
+
const arr = out.get(k);
|
|
42
|
+
if (arr)
|
|
43
|
+
arr.push(v);
|
|
44
|
+
else
|
|
45
|
+
out.set(k, [v]);
|
|
46
|
+
};
|
|
47
|
+
const visit = (prefix, v) => {
|
|
48
|
+
if (Array.isArray(v)) {
|
|
49
|
+
if (prefix)
|
|
50
|
+
add(prefix, v); // 배열 자체도 등록
|
|
51
|
+
for (const item of v) {
|
|
52
|
+
if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
53
|
+
for (const [k, val] of Object.entries(item)) {
|
|
54
|
+
visit(prefix ? `${prefix}.${k}` : k, val);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (v && typeof v === "object") {
|
|
60
|
+
if (prefix)
|
|
61
|
+
add(prefix, v); // 객체 자체도 등록
|
|
62
|
+
for (const [k, val] of Object.entries(v)) {
|
|
63
|
+
visit(prefix ? `${prefix}.${k}` : k, val);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (prefix)
|
|
68
|
+
add(prefix, v);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
for (const [k, v] of Object.entries(payload))
|
|
72
|
+
visit(k, v);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** 본문에서 query 가 포함된 줄(앞뒤 줄 포함) 발췌 */
|
|
76
|
+
function snippetLines(md, query, maxHits = 5) {
|
|
77
|
+
const q = query.toLowerCase();
|
|
78
|
+
const lines = md.split("\n");
|
|
79
|
+
const hits = [];
|
|
80
|
+
for (let i = 0; i < lines.length && hits.length < maxHits; i++) {
|
|
81
|
+
if (lines[i].toLowerCase().includes(q)) {
|
|
82
|
+
hits.push(lines[i].trim());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return hits;
|
|
86
|
+
}
|
|
87
|
+
// -- auto-check 함수 --
|
|
88
|
+
function autoCheckPii(obj) {
|
|
89
|
+
// 객체의 문자열 값들을 개별 검사 (JSON 직렬화 시 인접 숫자들이 false-positive 유발 방지)
|
|
90
|
+
const hits = [];
|
|
91
|
+
// 앞뒤에 추가 숫자가 없을 때만 매칭 (timestamp 같은 긴 숫자에서 부분 매칭 회피)
|
|
92
|
+
const RE_RRN = /(?<!\d)\d{6}-?[1-4]\d{6}(?!\d)/; // 주민번호 13자리
|
|
93
|
+
const RE_CARD = /(?<!\d)\d{4}-?\d{4}-?\d{4}-?\d{4}(?!\d)/; // 카드 16자리
|
|
94
|
+
const RE_PHONE = /(?<!\d)01[0-9]-?\d{3,4}-?\d{4}(?!\d)/; // 휴대전화 10~11자리
|
|
95
|
+
const visit = (v) => {
|
|
96
|
+
if (typeof v === "string") {
|
|
97
|
+
if (RE_RRN.test(v) && !hits.includes("주민번호 패턴"))
|
|
98
|
+
hits.push("주민번호 패턴");
|
|
99
|
+
if (RE_CARD.test(v) && !hits.includes("카드번호 패턴"))
|
|
100
|
+
hits.push("카드번호 패턴");
|
|
101
|
+
if (RE_PHONE.test(v) && !hits.includes("전화번호 패턴"))
|
|
102
|
+
hits.push("전화번호 패턴");
|
|
103
|
+
}
|
|
104
|
+
else if (Array.isArray(v)) {
|
|
105
|
+
for (const x of v)
|
|
106
|
+
visit(x);
|
|
107
|
+
}
|
|
108
|
+
else if (v && typeof v === "object") {
|
|
109
|
+
for (const x of Object.values(v))
|
|
110
|
+
visit(x);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
visit(obj);
|
|
114
|
+
return hits.length === 0
|
|
115
|
+
? { pass: true }
|
|
116
|
+
: { pass: false, message: `감지된 패턴: ${hits.join(", ")} — 운영 로그 사용 시 마스킹 후 검증하세요` };
|
|
117
|
+
}
|
|
118
|
+
function autoCheckEncoding(obj) {
|
|
119
|
+
const str = JSON.stringify(obj);
|
|
120
|
+
const match = str.match(/%[0-9A-Fa-f]{2}|%u[0-9A-Fa-f]{4}/);
|
|
121
|
+
return match
|
|
122
|
+
? { pass: false, message: `URL 인코딩 문자 감지('${match[0]}'). 디코딩 후 전달하세요.` }
|
|
123
|
+
: { pass: true };
|
|
124
|
+
}
|
|
125
|
+
function autoCheckWhitespace(obj) {
|
|
126
|
+
const issues = Object.entries(obj)
|
|
127
|
+
.filter(([, v]) => typeof v === "string" && (/^\s/.test(v) || /\s$/.test(v)))
|
|
128
|
+
.map(([k]) => k);
|
|
129
|
+
return issues.length === 0
|
|
130
|
+
? { pass: true }
|
|
131
|
+
: { pass: false, message: `앞뒤 공백 감지 필드: ${issues.join(", ")}` };
|
|
132
|
+
}
|
|
133
|
+
function autoCheckAmtSum(obj) {
|
|
134
|
+
const total = obj["totalPaymentAmt"];
|
|
135
|
+
const tf = obj["totalTaxfreeAmt"];
|
|
136
|
+
const ta = obj["totalTaxableAmt"];
|
|
137
|
+
const vat = obj["totalVatAmt"];
|
|
138
|
+
if (typeof total !== "number" ||
|
|
139
|
+
typeof tf !== "number" ||
|
|
140
|
+
typeof ta !== "number" ||
|
|
141
|
+
typeof vat !== "number")
|
|
142
|
+
return null;
|
|
143
|
+
const expected = tf + ta + vat;
|
|
144
|
+
return total === expected
|
|
145
|
+
? { pass: true }
|
|
146
|
+
: { pass: false, message: `${total} ≠ ${tf} + ${ta} + ${vat} (= ${expected})` };
|
|
147
|
+
}
|
|
148
|
+
function autoCheckReturnUrl(obj) {
|
|
149
|
+
if (!("returnUrl" in obj))
|
|
150
|
+
return null;
|
|
151
|
+
const val = obj["returnUrl"];
|
|
152
|
+
if (typeof val !== "string")
|
|
153
|
+
return null;
|
|
154
|
+
return /^https?:\/\//.test(val)
|
|
155
|
+
? { pass: true }
|
|
156
|
+
: { pass: false, message: `http:// 또는 https://로 시작해야 함. 현재: '${val}'` };
|
|
157
|
+
}
|
|
158
|
+
function makeEnumCheck(field, allowed) {
|
|
159
|
+
return (obj) => {
|
|
160
|
+
if (!(field in obj))
|
|
161
|
+
return null;
|
|
162
|
+
const val = obj[field];
|
|
163
|
+
if (typeof val !== "string")
|
|
164
|
+
return null;
|
|
165
|
+
return allowed.includes(val)
|
|
166
|
+
? { pass: true }
|
|
167
|
+
: { pass: false, message: `'${val}' — 허용값: ${allowed.join(" | ")}` };
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function makeDateYmdtCheck(fields) {
|
|
171
|
+
return (obj) => {
|
|
172
|
+
const present = fields.filter((f) => f in obj && typeof obj[f] === "string");
|
|
173
|
+
if (present.length === 0)
|
|
174
|
+
return null;
|
|
175
|
+
const fails = present.filter((f) => !/^\d{14}$/.test(obj[f]));
|
|
176
|
+
return fails.length === 0
|
|
177
|
+
? { pass: true }
|
|
178
|
+
: { pass: false, message: `YYYYMMDDHHmmss 14자리 필요: ${fails.map((f) => `${f}='${obj[f]}'`).join(", ")}` };
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function makeDateYmdCheck(field, allowFull = false) {
|
|
182
|
+
return (obj) => {
|
|
183
|
+
if (!(field in obj) || typeof obj[field] !== "string")
|
|
184
|
+
return null;
|
|
185
|
+
const val = obj[field];
|
|
186
|
+
const ok = /^\d{8}$/.test(val) || (allowFull && /^\d{14}$/.test(val));
|
|
187
|
+
return ok
|
|
188
|
+
? { pass: true }
|
|
189
|
+
: { pass: false, message: `${allowFull ? "YYYYMMDD(8자리) 또는 YYYYMMDDHHmmss(14자리)" : "YYYYMMDD(8자리)"} 필요. 현재: '${val}'` };
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// -- 공통 체크 항목 --
|
|
193
|
+
const ITEMS_COMMON = [
|
|
194
|
+
{
|
|
195
|
+
id: "common-pii",
|
|
196
|
+
category: "보안",
|
|
197
|
+
title: "개인정보 파라미터 미포함",
|
|
198
|
+
detail: "주민번호·카드번호·전화번호 등 개인정보가 request 파라미터에 유입되지 않아야 함",
|
|
199
|
+
autoCheck: autoCheckPii,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "common-encoding",
|
|
203
|
+
category: "기타",
|
|
204
|
+
title: "파라미터 URL 인코딩 문자 미포함",
|
|
205
|
+
detail: "'%XX' 또는 '%uXXXX' 형태의 인코딩 문자가 파라미터값에 포함되면 안 됨",
|
|
206
|
+
autoCheck: autoCheckEncoding,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "common-whitespace",
|
|
210
|
+
category: "기타",
|
|
211
|
+
title: "파라미터 앞뒤 공백 없음",
|
|
212
|
+
detail: "문자열 파라미터 값의 앞/뒤에 불필요한 공백이 없어야 함",
|
|
213
|
+
autoCheck: autoCheckWhitespace,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "common-amt-sum",
|
|
217
|
+
category: "결제금액",
|
|
218
|
+
title: "금액 합산 일치 (totalPaymentAmt = 면세 + 과세 + 부가세)",
|
|
219
|
+
autoCheck: autoCheckAmtSum,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: "common-return-url",
|
|
223
|
+
category: "결제인증/승인",
|
|
224
|
+
title: "returnUrl HTTP(S) 형식",
|
|
225
|
+
autoCheck: autoCheckReturnUrl,
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
// -- 온라인결제 체크리스트 --
|
|
229
|
+
const CHECKLIST_ONLINE = [
|
|
230
|
+
...ITEMS_COMMON,
|
|
231
|
+
{ id: "online-firewall", category: "사전준비", title: "PAYCO 방화벽 등록 완료" },
|
|
232
|
+
{ id: "online-merchant-code", category: "사전준비", title: "PAYCO 가맹점 코드 발급 완료" },
|
|
233
|
+
{
|
|
234
|
+
id: "online-btn-design",
|
|
235
|
+
category: "결제노출",
|
|
236
|
+
title: "PAYCO 버튼 디자인 가이드 준수",
|
|
237
|
+
detail: "PAYCO 영문자·붉은색 원칙. 별도 요청 시 버튼 이미지·PSD 제공",
|
|
238
|
+
},
|
|
239
|
+
{ id: "online-btn-show", category: "결제노출", title: "주문서에 PAYCO 결제 버튼 정상 노출" },
|
|
240
|
+
{
|
|
241
|
+
id: "online-popup",
|
|
242
|
+
category: "결제노출",
|
|
243
|
+
title: "결제창 팝업 정상 노출",
|
|
244
|
+
detail: "'결제하기' 클릭 시 PAYCO 결제창(popup)이 열려야 함",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "online-popup-block",
|
|
248
|
+
category: "결제노출",
|
|
249
|
+
title: "팝업 차단 미적용 또는 허용 안내 Alert 노출",
|
|
250
|
+
detail: "팝업 차단 해제가 어렵다면 사용자에게 허용 요청 Alert 노출",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: "online-order-method",
|
|
254
|
+
category: "결제인증/승인",
|
|
255
|
+
title: "orderMethod 유효값 (CHECKOUT | EASYPAY | EASYPAY_F)",
|
|
256
|
+
autoCheck: makeEnumCheck("orderMethod", ["CHECKOUT", "EASYPAY", "EASYPAY_F"]),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "online-order-channel",
|
|
260
|
+
category: "결제인증/승인",
|
|
261
|
+
title: "orderChannel 유효값 (PC | MOBILE)",
|
|
262
|
+
autoCheck: makeEnumCheck("orderChannel", ["PC", "MOBILE"]),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: "online-inapp",
|
|
266
|
+
category: "결제인증/승인",
|
|
267
|
+
title: "inAppYn 유효값 (Y | N)",
|
|
268
|
+
autoCheck: makeEnumCheck("inAppYn", ["Y", "N"]),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "online-date",
|
|
272
|
+
category: "결제인증/승인",
|
|
273
|
+
title: "날짜 형식 YYYYMMDDHHmmss (payExpiryYmdt 등)",
|
|
274
|
+
autoCheck: makeDateYmdtCheck(["payExpiryYmdt"]),
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: "online-vbank-date",
|
|
278
|
+
category: "결제인증/승인",
|
|
279
|
+
title: "가상계좌 만료일 형식 (YYYYMMDD 또는 YYYYMMDDHHmmss)",
|
|
280
|
+
autoCheck: makeDateYmdCheck("virtualAccountExpiryYmd", true),
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
id: "online-amt-match",
|
|
284
|
+
category: "결제인증/승인",
|
|
285
|
+
title: "주문금액 ↔ PAYCO 결제창 최종결제금액 일치 여부 육안 확인",
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: "online-auth-recv",
|
|
289
|
+
category: "결제인증/승인",
|
|
290
|
+
title: "결제인증 정보(인증토큰·주문예약번호) 정상 수신",
|
|
291
|
+
detail: "PAYCO 결제비밀번호 인증 후 returnUrl로 인증정보가 정상 전달되는지 확인",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: "online-timeout",
|
|
295
|
+
category: "결제인증/승인",
|
|
296
|
+
title: "승인결과 미수신 시 망취소 처리",
|
|
297
|
+
detail: "통신장애·타임아웃 등으로 미수신 시 결제취소(망취소) 또는 실패 인지 처리 구현",
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: "online-cancel",
|
|
301
|
+
category: "결제취소",
|
|
302
|
+
title: "전체취소 / 부분취소 정상 동작",
|
|
303
|
+
detail: "PAYCO 파트너센터(partner.payco.com)에서도 취소 가능",
|
|
304
|
+
},
|
|
305
|
+
{ id: "online-cancel-fail", category: "결제취소", title: "취소 실패 시 실패 사유(에러코드·메시지) 노출" },
|
|
306
|
+
{ id: "online-receipt", category: "영수증", title: "영수증 API 구현 및 주문명(상품명) 정상 노출" },
|
|
307
|
+
];
|
|
308
|
+
// -- 오프라인결제 체크리스트 --
|
|
309
|
+
const CHECKLIST_OFFLINE = [
|
|
310
|
+
...ITEMS_COMMON,
|
|
311
|
+
{ id: "offline-btn", category: "결제노출", title: "단말기에 PAYCO 결제 버튼 정상 노출 (디자인 가이드 준수)" },
|
|
312
|
+
{ id: "offline-receipt", category: "공통", title: "영수증 필요 정보 정상 노출" },
|
|
313
|
+
{ id: "offline-approval", category: "결제처리", title: "승인요청 후 승인결과 정상 수신" },
|
|
314
|
+
{ id: "offline-fail", category: "결제처리", title: "결제 실패 시 실패 사유 노출" },
|
|
315
|
+
{ id: "offline-timeout", category: "결제처리", title: "승인결과 미수신 시 망취소 처리" },
|
|
316
|
+
{
|
|
317
|
+
id: "offline-success",
|
|
318
|
+
category: "결제처리",
|
|
319
|
+
title: "단독결제 / 복합결제(+쿠폰) 등 다양한 케이스 성공 확인",
|
|
320
|
+
},
|
|
321
|
+
{ id: "offline-cancel", category: "결제취소", title: "결제취소 성공 확인" },
|
|
322
|
+
{
|
|
323
|
+
id: "offline-dll-lifecycle",
|
|
324
|
+
category: "VCAT 연동",
|
|
325
|
+
title: "DLL 연결/해제를 프로그램 시작·종료 시 1회만 수행 (결제마다 반복 금지)",
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: "offline-dll-func",
|
|
329
|
+
category: "VCAT 연동",
|
|
330
|
+
title: "DLL 함수 사용 확인",
|
|
331
|
+
detail: "NfcDailyRun 구동 / NfcInit 초기화(SetPaycoData 전) / NfcASyncRun 후 NfcSendASyncEnd 호출",
|
|
332
|
+
},
|
|
333
|
+
{ id: "offline-signature", category: "API 연동", title: "Signature 생성 및 결제 승인·취소 정상 동작 확인" },
|
|
334
|
+
{
|
|
335
|
+
id: "offline-pos-reg",
|
|
336
|
+
category: "API 연동",
|
|
337
|
+
title: "POS 등록 API 언제든 호출 가능",
|
|
338
|
+
detail: "가맹점·단말기 정보 변경 시 재호출 가능한지 확인",
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
// -- 오더클라우드 체크리스트 --
|
|
342
|
+
const CHECKLIST_ORDER = [
|
|
343
|
+
...ITEMS_COMMON,
|
|
344
|
+
{ id: "order-receipt", category: "공통", title: "영수증 필요 정보 정상 노출" },
|
|
345
|
+
{ id: "order-sales", category: "매출처리", title: "주문 수락 시 POS 단말기에 매출 정상 등록" },
|
|
346
|
+
{
|
|
347
|
+
id: "order-dll-daily",
|
|
348
|
+
category: "VORDER 연동",
|
|
349
|
+
title: "단말기 구동 시 DailyRun 함수 사용 확인",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: "order-dll-senddata",
|
|
353
|
+
category: "VORDER 연동",
|
|
354
|
+
title: "수락·완료·취소 등 윈도우 이벤트 수신 시 SendData 처리 확인",
|
|
355
|
+
detail: "일시적 POS 오류 발생 시에도 SendData 처리가 되는지 확인",
|
|
356
|
+
},
|
|
357
|
+
{ id: "order-dll-end", category: "VORDER 연동", title: "종료 시 ProcessEnd 함수 호출 확인" },
|
|
358
|
+
{ id: "order-soldout", category: "VORDER 연동", title: "품절 / 품절해제 정상 동작" },
|
|
359
|
+
{
|
|
360
|
+
id: "order-light-query",
|
|
361
|
+
category: "라이트 VORDER",
|
|
362
|
+
title: "QueryInfo 함수 및 조회 조건 파라미터 정상 호출 확인",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: "order-light-bulk",
|
|
366
|
+
category: "라이트 VORDER",
|
|
367
|
+
title: "수백 건 이상 대량 주문건 매출 연동 이상 없음",
|
|
368
|
+
detail: "결제·할인 수단별 정상 집계도 확인",
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
// -- 멤버십 클라우드 체크리스트 --
|
|
372
|
+
const CHECKLIST_MEMBERSHIP = [
|
|
373
|
+
...ITEMS_COMMON,
|
|
374
|
+
{ id: "mem-vmem-run", category: "VMEM 연동", title: "VMEM 정상 실행 확인" },
|
|
375
|
+
{
|
|
376
|
+
id: "mem-dll-daily",
|
|
377
|
+
category: "VMEM 연동",
|
|
378
|
+
title: "단말기 구동 시 ExecDailyRun 함수 사용 확인",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: "mem-dll-init",
|
|
382
|
+
category: "VMEM 연동",
|
|
383
|
+
title: "비동기 주문 승인 전 MemInit 초기화 확인",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "mem-dll-product",
|
|
387
|
+
category: "VMEM 연동",
|
|
388
|
+
title: "주문 승인 이후 완료 시점까지 상품 정보(종류·가격) 변경 없음",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: "mem-dll-ext-cancel",
|
|
392
|
+
category: "VMEM 연동",
|
|
393
|
+
title: "주문 취소 시 외부결제 취소처리 먼저 진행",
|
|
394
|
+
},
|
|
395
|
+
{ id: "mem-dll-side-effect", category: "VMEM 연동", title: "DLL 함수 결과가 외부결제에 영향 없음" },
|
|
396
|
+
{
|
|
397
|
+
id: "mem-kiosk",
|
|
398
|
+
category: "VMEM 기능",
|
|
399
|
+
title: "KIOSK 모드 시 제휴사 설정·스펙에 맞는 화면 노출",
|
|
400
|
+
},
|
|
401
|
+
{ id: "mem-comm", category: "VMEM 기능", title: "DLL ↔ EXE 통신 원활" },
|
|
402
|
+
{ id: "mem-query", category: "API 연동", title: "멤버십 회원 및 재화 정상 조회" },
|
|
403
|
+
{
|
|
404
|
+
id: "mem-process",
|
|
405
|
+
category: "API 연동",
|
|
406
|
+
title: "멤버십 재화 사용 주문 생성~완료 프로세스 이상 없음",
|
|
407
|
+
},
|
|
408
|
+
{ id: "mem-fail", category: "API 연동", title: "멤버십 처리 실패 시 실패 사유 노출" },
|
|
409
|
+
{
|
|
410
|
+
id: "mem-timeout",
|
|
411
|
+
category: "API 연동",
|
|
412
|
+
title: "승인결과 미수신 시 멤버십 취소 또는 실패 처리",
|
|
413
|
+
detail: "통신장애·타임아웃 등으로 미수신 시 대응 구현",
|
|
414
|
+
},
|
|
415
|
+
{ id: "mem-cancel", category: "API 연동", title: "멤버십 취소 다양한 케이스 이상 없음" },
|
|
416
|
+
];
|
|
417
|
+
export const CHECKLISTS = {
|
|
418
|
+
online: CHECKLIST_ONLINE,
|
|
419
|
+
offline: CHECKLIST_OFFLINE,
|
|
420
|
+
order: CHECKLIST_ORDER,
|
|
421
|
+
membership: CHECKLIST_MEMBERSHIP,
|
|
422
|
+
};
|
|
423
|
+
export const SERVICE_LABELS = {
|
|
424
|
+
online: "온라인결제",
|
|
425
|
+
offline: "오프라인결제",
|
|
426
|
+
order: "오더클라우드",
|
|
427
|
+
membership: "멤버십 클라우드",
|
|
428
|
+
};
|
|
429
|
+
/** API 슬러그에서 서비스명 추출 (예: "online-easypay-01" → "online") */
|
|
430
|
+
function serviceOf(apiSlug) {
|
|
431
|
+
return apiSlug.split("-")[0];
|
|
432
|
+
}
|
|
433
|
+
/** 체크리스트를 카테고리별로 그룹화 */
|
|
434
|
+
function groupByCategory(items) {
|
|
435
|
+
const map = new Map();
|
|
436
|
+
for (const item of items) {
|
|
437
|
+
if (!map.has(item.category))
|
|
438
|
+
map.set(item.category, []);
|
|
439
|
+
map.get(item.category).push(item);
|
|
440
|
+
}
|
|
441
|
+
return map;
|
|
442
|
+
}
|
|
443
|
+
// ---------- 도구 등록 ----------
|
|
444
|
+
export function registerAllTools(server, store) {
|
|
445
|
+
const slugs = store.index.categories.map((c) => c.slug);
|
|
446
|
+
const slugList = slugs.join(", ");
|
|
447
|
+
// 1) search_devcenter ----------------------------------------------------
|
|
448
|
+
server.registerTool("search_devcenter", {
|
|
449
|
+
title: "개발자센터 검색",
|
|
450
|
+
description: "PAYCO 개발자센터 가이드(온라인결제·자동결제)를 키워드로 검색. " +
|
|
451
|
+
"제목·섹션·본문 텍스트에서 매칭되는 가이드와 발췌를 반환.",
|
|
452
|
+
inputSchema: {
|
|
453
|
+
query: z.string().min(1).describe("검색어 (예: '결제 승인', 'orderProducts')"),
|
|
454
|
+
},
|
|
455
|
+
}, async ({ query }) => {
|
|
456
|
+
const results = [];
|
|
457
|
+
for (const cat of store.index.categories) {
|
|
458
|
+
const titleHit = cat.title.includes(query) ||
|
|
459
|
+
cat.keywords.some((k) => k.includes(query)) ||
|
|
460
|
+
cat.sections.some((s) => s.label.includes(query));
|
|
461
|
+
const md = store.guides.get(cat.slug) ?? "";
|
|
462
|
+
const snippets = snippetLines(md, query, 5);
|
|
463
|
+
if (titleHit || snippets.length > 0) {
|
|
464
|
+
let block = `## [${cat.slug}] ${cat.title}\n`;
|
|
465
|
+
block += ` ${cat.url}\n`;
|
|
466
|
+
if (snippets.length > 0) {
|
|
467
|
+
for (const s of snippets)
|
|
468
|
+
block += ` > ${s}\n`;
|
|
469
|
+
}
|
|
470
|
+
results.push(block);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (results.length === 0) {
|
|
474
|
+
return text(`'${query}' 검색 결과 없음. 사용 가능한 슬러그: ${slugList}`);
|
|
475
|
+
}
|
|
476
|
+
return text(`검색어: "${query}" — ${results.length}건\n\n${results.join("\n")}`);
|
|
477
|
+
});
|
|
478
|
+
// 2) get_guide ----------------------------------------------------------
|
|
479
|
+
server.registerTool("get_guide", {
|
|
480
|
+
title: "가이드 본문 조회",
|
|
481
|
+
description: `특정 가이드의 전체 본문(Markdown)을 반환. 사용 가능한 슬러그: ${slugList}`,
|
|
482
|
+
inputSchema: {
|
|
483
|
+
slug: z
|
|
484
|
+
.string()
|
|
485
|
+
.describe(`가이드 슬러그 (${slugList})`),
|
|
486
|
+
},
|
|
487
|
+
}, async ({ slug }) => {
|
|
488
|
+
const md = store.guides.get(slug);
|
|
489
|
+
if (!md) {
|
|
490
|
+
return notFound(`'${slug}' 가이드 없음. 사용 가능한 슬러그: ${slugList}`);
|
|
491
|
+
}
|
|
492
|
+
return text(md);
|
|
493
|
+
});
|
|
494
|
+
// 3) list_apis ----------------------------------------------------------
|
|
495
|
+
server.registerTool("list_apis", {
|
|
496
|
+
title: "가이드 / API 목록",
|
|
497
|
+
description: "PAYCO 개발자센터에 정리된 가이드 카테고리와 각 카테고리의 단계(섹션) 목록을 반환. " +
|
|
498
|
+
"외부 제휴사가 어떤 가이드/단계가 있는지 한눈에 파악할 때 사용.",
|
|
499
|
+
inputSchema: {},
|
|
500
|
+
}, async () => {
|
|
501
|
+
const lines = [];
|
|
502
|
+
lines.push(`# PAYCO 개발자센터 가이드 목록 (${store.index.scope})`);
|
|
503
|
+
lines.push("");
|
|
504
|
+
for (const cat of store.index.categories) {
|
|
505
|
+
lines.push(`## [${cat.slug}] ${cat.title}`);
|
|
506
|
+
lines.push(` ${cat.url}`);
|
|
507
|
+
if (cat.sections.length > 0) {
|
|
508
|
+
for (const s of cat.sections) {
|
|
509
|
+
lines.push(` - ${s.label}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
lines.push("");
|
|
513
|
+
}
|
|
514
|
+
return text(lines.join("\n"));
|
|
515
|
+
});
|
|
516
|
+
// 4) get_api_spec -------------------------------------------------------
|
|
517
|
+
server.registerTool("get_api_spec", {
|
|
518
|
+
title: "API 명세 조회",
|
|
519
|
+
description: "특정 카테고리(easypay, autopay 등)의 API 명세·파라미터·응답 형식을 담은 가이드를 반환. " +
|
|
520
|
+
"현재 버전은 가이드 전체를 돌려주고, LLM 이 해당 API 섹션을 활용.",
|
|
521
|
+
inputSchema: {
|
|
522
|
+
category: z
|
|
523
|
+
.string()
|
|
524
|
+
.describe(`카테고리 슬러그 (${slugList})`),
|
|
525
|
+
},
|
|
526
|
+
}, async ({ category }) => {
|
|
527
|
+
const md = store.guides.get(category);
|
|
528
|
+
if (!md) {
|
|
529
|
+
return notFound(`'${category}' 카테고리 없음. 사용 가능: ${slugList}`);
|
|
530
|
+
}
|
|
531
|
+
return text(md);
|
|
532
|
+
});
|
|
533
|
+
// 5) get_sample_code ----------------------------------------------------
|
|
534
|
+
server.registerTool("get_sample_code", {
|
|
535
|
+
title: "샘플 코드 조회",
|
|
536
|
+
description: "특정 가이드의 코드 블록만 추출해 반환. 가이드 슬러그와(선택적으로) 언어를 지정.",
|
|
537
|
+
inputSchema: {
|
|
538
|
+
slug: z
|
|
539
|
+
.string()
|
|
540
|
+
.describe(`가이드 슬러그 (${slugList})`),
|
|
541
|
+
language: z
|
|
542
|
+
.string()
|
|
543
|
+
.optional()
|
|
544
|
+
.describe("필터할 언어 태그 (예: json, java, javascript). 미지정 시 모든 코드 블록 반환."),
|
|
545
|
+
},
|
|
546
|
+
}, async ({ slug, language }) => {
|
|
547
|
+
const md = store.guides.get(slug);
|
|
548
|
+
if (!md) {
|
|
549
|
+
return notFound(`'${slug}' 가이드 없음. 사용 가능한 슬러그: ${slugList}`);
|
|
550
|
+
}
|
|
551
|
+
const blocks = extractCodeBlocks(md, language);
|
|
552
|
+
if (blocks.length === 0) {
|
|
553
|
+
return text(`'${slug}' 가이드에 ${language ? `'${language}' ` : ""}코드 블록 없음.`);
|
|
554
|
+
}
|
|
555
|
+
const header = `# ${slug} — 코드 블록 ${blocks.length}개${language ? ` (${language})` : ""}\n\n`;
|
|
556
|
+
return text(header + blocks.join("\n\n"));
|
|
557
|
+
});
|
|
558
|
+
// 6) list_validatable_apis ----------------------------------------------
|
|
559
|
+
server.registerTool("list_validatable_apis", {
|
|
560
|
+
title: "검증 가능한 API 목록",
|
|
561
|
+
description: "validate_payload 로 자동 검증 가능한 PAYCO API 목록(슬러그·URL·메서드·파라미터 수)을 반환. " +
|
|
562
|
+
"외부 제휴사가 자신의 요청 로그를 어떤 API 슬러그로 검증해야 할지 확인하는 용도.",
|
|
563
|
+
inputSchema: {},
|
|
564
|
+
}, async () => {
|
|
565
|
+
const lines = [];
|
|
566
|
+
lines.push(`# 검증 가능한 API (${store.apiSpecs.api_count}개)`);
|
|
567
|
+
lines.push("");
|
|
568
|
+
for (const api of store.apiSpecs.apis) {
|
|
569
|
+
const url = api.url ?? "(URL 정보 없음)";
|
|
570
|
+
const method = api.method ?? "?";
|
|
571
|
+
const req = api.request_params.length;
|
|
572
|
+
const reqRequired = api.request_params.filter((p) => p.required).length;
|
|
573
|
+
lines.push(`- **${api.slug}** [${method}] ${url}`);
|
|
574
|
+
lines.push(` title: ${api.title}`);
|
|
575
|
+
lines.push(` request: ${req} params (${reqRequired} required) / response: ${api.response_params.length} params`);
|
|
576
|
+
}
|
|
577
|
+
return text(lines.join("\n"));
|
|
578
|
+
});
|
|
579
|
+
// 7) get_checklist ------------------------------------------------------
|
|
580
|
+
server.registerTool("get_checklist", {
|
|
581
|
+
title: "서비스별 사전 검수 체크리스트",
|
|
582
|
+
description: "PAYCO 연동 완료 후 자체 검수 시 사용하는 서비스별 사전 체크리스트를 반환. " +
|
|
583
|
+
"자동 검증 가능한 항목(payload 기반)과 수동 확인이 필요한 항목을 구분해 표시.",
|
|
584
|
+
inputSchema: {
|
|
585
|
+
service: z
|
|
586
|
+
.enum(["online", "offline", "order", "membership"])
|
|
587
|
+
.describe("서비스 유형: online(온라인결제) | offline(오프라인결제) | order(오더클라우드) | membership(멤버십 클라우드)"),
|
|
588
|
+
},
|
|
589
|
+
}, async ({ service }) => {
|
|
590
|
+
const checklist = CHECKLISTS[service] ?? ITEMS_COMMON;
|
|
591
|
+
const label = SERVICE_LABELS[service] ?? service;
|
|
592
|
+
const autoItems = checklist.filter((c) => c.autoCheck);
|
|
593
|
+
const manualItems = checklist.filter((c) => !c.autoCheck);
|
|
594
|
+
const lines = [];
|
|
595
|
+
lines.push(`# PAYCO ${label} 사전 검수 체크리스트 (${checklist.length}개)`);
|
|
596
|
+
lines.push("");
|
|
597
|
+
lines.push(`> validate_payload 호출 시 자동 검증 항목(${autoItems.length}개)은 payload 기반으로 자동 확인됩니다.`);
|
|
598
|
+
lines.push(`> 수동 확인 항목(${manualItems.length}개)은 직접 검수가 필요합니다.`);
|
|
599
|
+
lines.push("");
|
|
600
|
+
if (autoItems.length > 0) {
|
|
601
|
+
lines.push(`## 자동 검증 항목 (${autoItems.length}개) — validate_payload 호출 시 자동 확인`);
|
|
602
|
+
const byCategory = groupByCategory(autoItems);
|
|
603
|
+
for (const [cat, items] of byCategory) {
|
|
604
|
+
lines.push(`\n**[${cat}]**`);
|
|
605
|
+
for (const item of items) {
|
|
606
|
+
lines.push(`- ☑ ${item.title}`);
|
|
607
|
+
if (item.detail)
|
|
608
|
+
lines.push(` → ${item.detail}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
lines.push("");
|
|
612
|
+
}
|
|
613
|
+
if (manualItems.length > 0) {
|
|
614
|
+
lines.push(`## 수동 확인 항목 (${manualItems.length}개) — 직접 검수 필요`);
|
|
615
|
+
const byCategory = groupByCategory(manualItems);
|
|
616
|
+
for (const [cat, items] of byCategory) {
|
|
617
|
+
lines.push(`\n**[${cat}]**`);
|
|
618
|
+
for (const item of items) {
|
|
619
|
+
lines.push(`- ⬜ ${item.title}`);
|
|
620
|
+
if (item.detail)
|
|
621
|
+
lines.push(` → ${item.detail}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
lines.push("");
|
|
625
|
+
}
|
|
626
|
+
const checkGuideSlug = `${service}-check`;
|
|
627
|
+
lines.push(`> 상세 검수 가이드: get_guide('${checkGuideSlug}') 또는 https://devcenter.payco.com/guide/${service}/check`);
|
|
628
|
+
return text(lines.join("\n"));
|
|
629
|
+
});
|
|
630
|
+
// 8) validate_payload ---------------------------------------------------
|
|
631
|
+
server.registerTool("validate_payload", {
|
|
632
|
+
title: "API 요청 페이로드 자동 검증 + 검수 체크리스트",
|
|
633
|
+
description: "PAYCO API 요청 페이로드(JSON)를 검증하고 서비스별 검수 체크리스트를 반환한다. " +
|
|
634
|
+
"반환값은 반드시 사용자에게 전체 그대로 보여줄 것 — 요약하거나 생략하지 말 것. " +
|
|
635
|
+
"체크리스트 항목은 ✓(자동통과) / ✗(자동실패, 이유 포함) / ⊘(필드 없어 스킵) / ⬜(수동확인 필요) 로 표시됨. " +
|
|
636
|
+
"⚠️ 운영 로그(실제 카드번호/주민번호/연락처)는 절대 그대로 붙여넣지 말 것.",
|
|
637
|
+
inputSchema: {
|
|
638
|
+
api: z
|
|
639
|
+
.string()
|
|
640
|
+
.describe("검증할 API 슬러그 (list_validatable_apis 로 확인). 예: online-easypay-01"),
|
|
641
|
+
payload: z
|
|
642
|
+
.string()
|
|
643
|
+
.describe("요청 페이로드 JSON 문자열. 예: '{\"sellerKey\":\"...\",\"totalPaymentAmt\":1000}'"),
|
|
644
|
+
},
|
|
645
|
+
}, async ({ api, payload }) => {
|
|
646
|
+
const spec = store.apiSpecs.apis.find((a) => a.slug === api);
|
|
647
|
+
if (!spec) {
|
|
648
|
+
const available = store.apiSpecs.apis.map((a) => a.slug).join(", ");
|
|
649
|
+
return notFound(`'${api}' API 명세 없음. 사용 가능: ${available}`);
|
|
650
|
+
}
|
|
651
|
+
// JSON 파싱
|
|
652
|
+
let obj;
|
|
653
|
+
try {
|
|
654
|
+
const parsed = JSON.parse(payload);
|
|
655
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
656
|
+
return notFound("payload 는 JSON 객체(중괄호)여야 합니다.");
|
|
657
|
+
}
|
|
658
|
+
obj = parsed;
|
|
659
|
+
}
|
|
660
|
+
catch (e) {
|
|
661
|
+
return notFound(`JSON 파싱 실패: ${e.message}`);
|
|
662
|
+
}
|
|
663
|
+
// --- API 명세 검증 (nested 평탄화 비교) ---
|
|
664
|
+
const specByName = new Map(spec.request_params.map((p) => [p.name, p]));
|
|
665
|
+
const flat = flattenPayload(obj);
|
|
666
|
+
const missingRequired = [];
|
|
667
|
+
const unknownParams = [];
|
|
668
|
+
const typeMismatches = [];
|
|
669
|
+
// 필수 누락: 명세 required 키가 평탄화된 payload 키에 없음
|
|
670
|
+
for (const p of spec.request_params) {
|
|
671
|
+
if (p.required && !flat.has(p.name)) {
|
|
672
|
+
missingRequired.push(`${p.name} (${p.type}) — ${p.description.slice(0, 60)}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// 알 수 없는 파라미터: payload 의 평탄화 키 중 명세에 없는 것
|
|
676
|
+
for (const key of flat.keys()) {
|
|
677
|
+
if (!specByName.has(key))
|
|
678
|
+
unknownParams.push(key);
|
|
679
|
+
}
|
|
680
|
+
// 타입 불일치: 평탄화 키마다 명세 타입과 실제 값 타입 비교 (배열 안 객체는 동일 키로 여러 값)
|
|
681
|
+
for (const [key, values] of flat.entries()) {
|
|
682
|
+
const p = specByName.get(key);
|
|
683
|
+
if (!p)
|
|
684
|
+
continue;
|
|
685
|
+
const expected = p.type.toLowerCase();
|
|
686
|
+
for (const value of values) {
|
|
687
|
+
const actual = Array.isArray(value) ? "array" : typeof value;
|
|
688
|
+
const ok = (expected.includes("string") && actual === "string") ||
|
|
689
|
+
(expected.includes("number") && actual === "number") ||
|
|
690
|
+
(expected.includes("boolean") && actual === "boolean") ||
|
|
691
|
+
(expected.includes("array") && actual === "array") ||
|
|
692
|
+
(expected.includes("object") && actual === "object") ||
|
|
693
|
+
expected === "unknown";
|
|
694
|
+
if (!ok) {
|
|
695
|
+
typeMismatches.push(`${key}: 명세 '${p.type}' / 실제 '${actual}'`);
|
|
696
|
+
break; // 같은 키의 다음 값들은 검사 스킵
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// --- 체크리스트 실행 ---
|
|
701
|
+
const service = serviceOf(api);
|
|
702
|
+
const checklist = CHECKLISTS[service] ?? ITEMS_COMMON;
|
|
703
|
+
const serviceLabel = SERVICE_LABELS[service] ?? service;
|
|
704
|
+
const runResults = checklist.map((item) => ({
|
|
705
|
+
item,
|
|
706
|
+
result: item.autoCheck ? item.autoCheck(obj) : null,
|
|
707
|
+
}));
|
|
708
|
+
const autoFailed = runResults.filter((r) => r.item.autoCheck && r.result !== null && !r.result.pass);
|
|
709
|
+
const autoPassed = runResults.filter((r) => r.item.autoCheck && r.result !== null && r.result.pass);
|
|
710
|
+
const autoSkipped = runResults.filter((r) => r.item.autoCheck && r.result === null);
|
|
711
|
+
const manualItems = runResults.filter((r) => !r.item.autoCheck);
|
|
712
|
+
const specFails = missingRequired.length + unknownParams.length + typeMismatches.length;
|
|
713
|
+
const checkFails = autoFailed.length;
|
|
714
|
+
const pass = specFails === 0 && checkFails === 0;
|
|
715
|
+
// ── 결과 버퍼에 저장 (엑셀 추출용) ─────────────────────
|
|
716
|
+
const { sellerOrderRefKey, reserveOrderNo } = extractOrderKeys(obj);
|
|
717
|
+
const specIssueRows = [];
|
|
718
|
+
for (const m of missingRequired) {
|
|
719
|
+
const fieldMatch = m.match(/^([^\s(]+)/);
|
|
720
|
+
specIssueRows.push({
|
|
721
|
+
kind: "missing_required",
|
|
722
|
+
field: fieldMatch?.[1] ?? "",
|
|
723
|
+
message: m,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
for (const u of unknownParams) {
|
|
727
|
+
specIssueRows.push({
|
|
728
|
+
kind: "unknown_param",
|
|
729
|
+
field: u,
|
|
730
|
+
message: `'${u}' 는 ${spec.slug} 명세에 없는 파라미터 (오타 또는 가이드 최신본 확인 필요)`,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
for (const t of typeMismatches) {
|
|
734
|
+
const fieldMatch = t.match(/^([^:]+):/);
|
|
735
|
+
specIssueRows.push({
|
|
736
|
+
kind: "type_mismatch",
|
|
737
|
+
field: fieldMatch?.[1]?.trim() ?? "",
|
|
738
|
+
message: t,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
const checklistRunRows = runResults.map(({ item, result }) => {
|
|
742
|
+
if (!item.autoCheck) {
|
|
743
|
+
return { category: item.category, title: item.title, status: "manual", detail: item.detail };
|
|
744
|
+
}
|
|
745
|
+
if (result === null) {
|
|
746
|
+
return { category: item.category, title: item.title, status: "skip" };
|
|
747
|
+
}
|
|
748
|
+
if (result.pass) {
|
|
749
|
+
return { category: item.category, title: item.title, status: "pass" };
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
category: item.category, title: item.title, status: "fail",
|
|
753
|
+
message: result.message,
|
|
754
|
+
};
|
|
755
|
+
});
|
|
756
|
+
const record = {
|
|
757
|
+
timestamp: new Date().toISOString(),
|
|
758
|
+
api: spec.slug,
|
|
759
|
+
endpoint: `[${spec.method ?? "?"}] ${spec.url ?? "(no url)"}`,
|
|
760
|
+
service,
|
|
761
|
+
serviceLabel,
|
|
762
|
+
sellerOrderRefKey,
|
|
763
|
+
reserveOrderNo,
|
|
764
|
+
pass,
|
|
765
|
+
specIssues: specIssueRows,
|
|
766
|
+
checklistRuns: checklistRunRows,
|
|
767
|
+
};
|
|
768
|
+
pushRecord(record);
|
|
769
|
+
const lines = [];
|
|
770
|
+
// ── 헤더 ──────────────────────────────────────────────
|
|
771
|
+
lines.push(`# ${pass ? "✓ PASS" : "✗ FAIL"} — ${spec.slug}`);
|
|
772
|
+
lines.push(`[${spec.method ?? "?"}] ${spec.url ?? "(no url)"} | ${spec.title}`);
|
|
773
|
+
lines.push(`서비스: ${serviceLabel} | 자동실패 ${checkFails + specFails}개`);
|
|
774
|
+
lines.push("");
|
|
775
|
+
// ── 검수 체크리스트 (항상 맨 앞에 전체 표시) ──────────
|
|
776
|
+
lines.push(`## 검수 체크리스트 — ${serviceLabel} (${checklist.length}개)`);
|
|
777
|
+
lines.push("");
|
|
778
|
+
// 카테고리별로 모든 항목을 한 번에 출력
|
|
779
|
+
const allByCategory = groupByCategory(checklist);
|
|
780
|
+
for (const [cat, items] of allByCategory) {
|
|
781
|
+
lines.push(`**[${cat}]**`);
|
|
782
|
+
for (const item of items) {
|
|
783
|
+
const run = runResults.find((r) => r.item.id === item.id);
|
|
784
|
+
if (!item.autoCheck) {
|
|
785
|
+
// 수동 확인
|
|
786
|
+
lines.push(`⬜ ${item.title}`);
|
|
787
|
+
if (item.detail)
|
|
788
|
+
lines.push(` → ${item.detail}`);
|
|
789
|
+
}
|
|
790
|
+
else if (run.result === null) {
|
|
791
|
+
// 스킵 (필드 없음)
|
|
792
|
+
lines.push(`⊘ ${item.title}`);
|
|
793
|
+
}
|
|
794
|
+
else if (run.result.pass) {
|
|
795
|
+
lines.push(`✓ ${item.title}`);
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
lines.push(`✗ ${item.title}`);
|
|
799
|
+
if (run.result.message)
|
|
800
|
+
lines.push(` → ${run.result.message}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
lines.push("");
|
|
804
|
+
}
|
|
805
|
+
// ── API 명세 검증 세부 결과 ────────────────────────────
|
|
806
|
+
if (specFails > 0) {
|
|
807
|
+
lines.push(`## API 명세 문제 (${specFails}개)`);
|
|
808
|
+
if (missingRequired.length > 0) {
|
|
809
|
+
lines.push(`\n**필수 누락 (${missingRequired.length})**`);
|
|
810
|
+
for (const m of missingRequired)
|
|
811
|
+
lines.push(`✗ ${m}`);
|
|
812
|
+
}
|
|
813
|
+
if (unknownParams.length > 0) {
|
|
814
|
+
lines.push(`\n**명세에 없는 파라미터 (${unknownParams.length})** — 오타 또는 가이드 최신본 확인`);
|
|
815
|
+
for (const u of unknownParams)
|
|
816
|
+
lines.push(`✗ ${u}`);
|
|
817
|
+
}
|
|
818
|
+
if (typeMismatches.length > 0) {
|
|
819
|
+
lines.push(`\n**타입 불일치 (${typeMismatches.length})**`);
|
|
820
|
+
for (const t of typeMismatches)
|
|
821
|
+
lines.push(`✗ ${t}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
lines.push("## API 명세 검증 ✓ — 이상 없음");
|
|
826
|
+
}
|
|
827
|
+
lines.push("");
|
|
828
|
+
lines.push(`참고 가이드: https://devcenter.payco.com/guide/${spec.guide.replace(/-/g, "/")}`);
|
|
829
|
+
lines.push("");
|
|
830
|
+
lines.push("---");
|
|
831
|
+
lines.push(`📊 **검수 결과 누적**: ${bufferSize()}건 — 검수가 끝났다면 사용자에게 **\"엑셀로 추출할까요?\"** 라고 물어보고, 원하면 \`export_validation_excel\` 도구를 호출하세요.`);
|
|
832
|
+
lines.push(" 엑셀은 검수요약 / 항목별 상세 / 공통검수체크리스트 3개 시트로 자동 생성됩니다.");
|
|
833
|
+
return text(lines.join("\n"));
|
|
834
|
+
});
|
|
835
|
+
// 9) export_validation_excel --------------------------------------------
|
|
836
|
+
server.registerTool("export_validation_excel", {
|
|
837
|
+
title: "검수 결과 엑셀(xlsx) 추출",
|
|
838
|
+
description: "지금까지 validate_payload 로 누적된 검수 결과를 엑셀(xlsx) 파일로 저장한다. " +
|
|
839
|
+
"시트 3개: (1) 검수요약 — API별 한 줄씩 PASS/FAIL, (2) 항목별 상세 — 검증 항목 단위로 펼침, " +
|
|
840
|
+
"(3) 공통검수체크리스트 — 4개 서비스 통합 체크리스트(자동 체크 가능한 항목은 자동 표시). " +
|
|
841
|
+
"**언제 호출**: validate_payload 검수가 끝난 뒤 사용자에게 엑셀 추출 여부를 묻고, 사용자가 동의했을 때만 호출.",
|
|
842
|
+
inputSchema: {
|
|
843
|
+
path: z
|
|
844
|
+
.string()
|
|
845
|
+
.optional()
|
|
846
|
+
.describe("저장할 파일 경로(절대 경로 권장). 미지정 시 %USERPROFILE%/Downloads/payco-validation-{timestamp}.xlsx 에 저장."),
|
|
847
|
+
clearAfter: z
|
|
848
|
+
.boolean()
|
|
849
|
+
.optional()
|
|
850
|
+
.describe("추출 후 버퍼를 초기화할지 여부 (기본값: false). true 면 새 검수 세션으로 분리 가능."),
|
|
851
|
+
},
|
|
852
|
+
}, async ({ path, clearAfter }) => {
|
|
853
|
+
const records = getAllRecords();
|
|
854
|
+
if (records.length === 0) {
|
|
855
|
+
return notFound("누적된 검수 결과가 없습니다. 먼저 validate_payload 로 1건 이상 검수를 수행하세요.");
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
const { path: outPath, recordCount, rowCount } = await exportValidationToExcel(records, path);
|
|
859
|
+
const cleared = clearAfter ? clearAllRecords() : 0;
|
|
860
|
+
const lines = [];
|
|
861
|
+
lines.push(`✅ 엑셀 추출 완료`);
|
|
862
|
+
lines.push("");
|
|
863
|
+
lines.push(`- 파일 경로: \`${outPath}\``);
|
|
864
|
+
lines.push(`- 포함 검수 건수: ${recordCount}건`);
|
|
865
|
+
lines.push(`- 항목별 상세 행 수: ${rowCount}행`);
|
|
866
|
+
lines.push(`- 시트: 검수요약 / 항목별 상세 / 공통검수체크리스트 (3개)`);
|
|
867
|
+
if (clearAfter) {
|
|
868
|
+
lines.push(`- 추출 후 버퍼 초기화: ${cleared}건 삭제됨`);
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
lines.push(`- 누적 버퍼 유지 (다음 검수 결과도 동일 버퍼에 누적됨)`);
|
|
872
|
+
}
|
|
873
|
+
lines.push("");
|
|
874
|
+
lines.push("Windows 탐색기에서 위 경로를 열어 결과를 확인하세요.");
|
|
875
|
+
return text(lines.join("\n"));
|
|
876
|
+
}
|
|
877
|
+
catch (e) {
|
|
878
|
+
return notFound(`엑셀 생성 실패: ${e.message}`);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
// 10) clear_validation_buffer -------------------------------------------
|
|
882
|
+
server.registerTool("clear_validation_buffer", {
|
|
883
|
+
title: "검수 결과 버퍼 초기화",
|
|
884
|
+
description: "누적된 검수 결과를 모두 비운다. 새 검수 세션을 시작하거나, 이전 결과를 엑셀로 추출하지 않고 폐기할 때 사용.",
|
|
885
|
+
inputSchema: {},
|
|
886
|
+
}, async () => {
|
|
887
|
+
const removed = clearAllRecords();
|
|
888
|
+
return text(`🗑 검수 결과 버퍼 초기화 — ${removed}건 삭제됨.`);
|
|
889
|
+
});
|
|
890
|
+
// 11) get_error_code ----------------------------------------------------
|
|
891
|
+
server.registerTool("get_error_code", {
|
|
892
|
+
title: "에러코드/코드값 조회",
|
|
893
|
+
description: "PAYCO 코드정리 표에서 특정 코드 값을 조회. API 응답코드(1000, 4802...), " +
|
|
894
|
+
"결제수단/주문유형/카드사/은행코드 등 모든 코드 카테고리를 한번에 검색.",
|
|
895
|
+
inputSchema: {
|
|
896
|
+
code: z
|
|
897
|
+
.string()
|
|
898
|
+
.min(1)
|
|
899
|
+
.describe("조회할 코드 (예: '1000', 'CHECKOUT', 'CCBC', '02')"),
|
|
900
|
+
},
|
|
901
|
+
}, async ({ code }) => {
|
|
902
|
+
const hits = [];
|
|
903
|
+
for (const cat of store.errorCodes.categories) {
|
|
904
|
+
const found = cat.items.find((it) => it.code === code);
|
|
905
|
+
if (found) {
|
|
906
|
+
hits.push(`[${cat.title}] ${found.code} = ${found.description}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (hits.length === 0) {
|
|
910
|
+
const partial = [];
|
|
911
|
+
for (const cat of store.errorCodes.categories) {
|
|
912
|
+
for (const it of cat.items) {
|
|
913
|
+
if (it.code.startsWith(code) || it.description.includes(code)) {
|
|
914
|
+
partial.push(`[${cat.title}] ${it.code} = ${it.description}`);
|
|
915
|
+
if (partial.length >= 10)
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (partial.length >= 10)
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
if (partial.length === 0) {
|
|
923
|
+
return notFound(`'${code}' 코드 없음. (정확/부분 매칭 모두 실패)`);
|
|
924
|
+
}
|
|
925
|
+
return text(`정확 매칭 없음. 부분 매칭 결과:\n${partial.join("\n")}`);
|
|
926
|
+
}
|
|
927
|
+
return text(hits.join("\n"));
|
|
928
|
+
});
|
|
929
|
+
}
|