@rhseung/ps-cli 1.8.0 → 1.9.1
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/README.md +39 -9
- package/dist/chunk-2YSOO6AM.js +370 -0
- package/dist/chunk-3H74PQRX.js +251 -0
- package/dist/chunk-457JZK3K.js +210 -0
- package/dist/chunk-INRLWS2O.js +414 -0
- package/dist/{chunk-4ISG24GW.js → chunk-LR64BN3N.js} +173 -15
- package/dist/chunk-OBO5XU4N.js +288 -0
- package/dist/{chunk-JPDN34C7.js → chunk-Q5NECGFA.js} +972 -116
- package/dist/{chunk-VIHXBCOZ.js → chunk-YZUGYJA4.js} +1 -1
- package/dist/commands/archive.js +6 -206
- package/dist/commands/config.js +74 -242
- package/dist/commands/fetch.js +7 -396
- package/dist/commands/init.js +50 -10
- package/dist/commands/open.js +6 -137
- package/dist/commands/run.js +45 -33
- package/dist/commands/search.js +269 -130
- package/dist/commands/stats.js +249 -82
- package/dist/commands/submit.js +7 -247
- package/dist/commands/test.js +7 -371
- package/dist/index.js +13 -50
- package/package.json +2 -1
- package/dist/chunk-3LR2NGRC.js +0 -55
- package/dist/chunk-7MQMPJ3X.js +0 -88
- package/dist/chunk-ASMT3CRD.js +0 -500
- package/dist/chunk-PY6GW22W.js +0 -13
- package/dist/chunk-QB2R47PW.js +0 -61
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getProblem,
|
|
4
|
+
scrapeProblem
|
|
5
|
+
} from "./chunk-LR64BN3N.js";
|
|
6
|
+
import {
|
|
7
|
+
Command,
|
|
8
|
+
CommandBuilder,
|
|
9
|
+
CommandDef,
|
|
10
|
+
__decorateClass,
|
|
11
|
+
defineFlags,
|
|
12
|
+
getAutoOpenEditor,
|
|
13
|
+
getEditor,
|
|
14
|
+
getIncludeTag,
|
|
15
|
+
getLanguageConfig,
|
|
16
|
+
getSolvingDirPath,
|
|
17
|
+
getSupportedLanguages,
|
|
18
|
+
getSupportedLanguagesString,
|
|
19
|
+
getTierColor,
|
|
20
|
+
getTierImageUrl,
|
|
21
|
+
getTierName,
|
|
22
|
+
logger,
|
|
23
|
+
parseTimeLimitToMs,
|
|
24
|
+
resolveProblemContext
|
|
25
|
+
} from "./chunk-Q5NECGFA.js";
|
|
26
|
+
|
|
27
|
+
// src/commands/fetch.tsx
|
|
28
|
+
import { StatusMessage, Alert, Spinner } from "@inkjs/ui";
|
|
29
|
+
import { Box as Box2 } from "ink";
|
|
30
|
+
|
|
31
|
+
// src/components/problem-dashboard.tsx
|
|
32
|
+
import { Box, Text } from "ink";
|
|
33
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
34
|
+
function ProblemDashboard({ problem }) {
|
|
35
|
+
const tierName = getTierName(problem.level);
|
|
36
|
+
const tierColor = getTierColor(problem.level);
|
|
37
|
+
const borderColorString = typeof tierColor === "string" ? tierColor : "#ff7ca8";
|
|
38
|
+
const textColorString = borderColorString;
|
|
39
|
+
return /* @__PURE__ */ jsx(
|
|
40
|
+
Box,
|
|
41
|
+
{
|
|
42
|
+
flexDirection: "column",
|
|
43
|
+
borderStyle: "round",
|
|
44
|
+
borderColor: borderColorString,
|
|
45
|
+
paddingX: 1,
|
|
46
|
+
alignSelf: "flex-start",
|
|
47
|
+
children: /* @__PURE__ */ jsxs(Text, { bold: true, color: textColorString, children: [
|
|
48
|
+
tierName,
|
|
49
|
+
" ",
|
|
50
|
+
/* @__PURE__ */ jsxs(Text, { color: "white", children: [
|
|
51
|
+
"#",
|
|
52
|
+
problem.id,
|
|
53
|
+
": ",
|
|
54
|
+
problem.title
|
|
55
|
+
] })
|
|
56
|
+
] })
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/hooks/use-fetch-problem.ts
|
|
62
|
+
import { execaCommand } from "execa";
|
|
63
|
+
import { useEffect, useState } from "react";
|
|
64
|
+
|
|
65
|
+
// src/services/file-generator.ts
|
|
66
|
+
import { existsSync } from "fs";
|
|
67
|
+
import { mkdir, writeFile, readFile } from "fs/promises";
|
|
68
|
+
import { join, dirname } from "path";
|
|
69
|
+
import { fileURLToPath } from "url";
|
|
70
|
+
function ensureTrailingNewline(content) {
|
|
71
|
+
if (!content || content.trim().length === 0) {
|
|
72
|
+
return content;
|
|
73
|
+
}
|
|
74
|
+
const trimmed = content.trimEnd();
|
|
75
|
+
if (trimmed.length === 0) {
|
|
76
|
+
return content;
|
|
77
|
+
}
|
|
78
|
+
const lines = trimmed.split("\n");
|
|
79
|
+
const lastLine = lines[lines.length - 1];
|
|
80
|
+
const isListItem = /^[\s]*[-*]\s/.test(lastLine) || /^[\s]*\d+[.)]\s/.test(lastLine);
|
|
81
|
+
const isTableRow = /^\s*\|.+\|\s*$/.test(lastLine);
|
|
82
|
+
const isCodeBlock = trimmed.endsWith("```");
|
|
83
|
+
const isImage = /!\[.*\]\(.*\)$/.test(trimmed);
|
|
84
|
+
if (isListItem || isTableRow || isCodeBlock || isImage) {
|
|
85
|
+
return trimmed + "\n\n";
|
|
86
|
+
}
|
|
87
|
+
if (!content.endsWith("\n")) {
|
|
88
|
+
return content + "\n\n";
|
|
89
|
+
}
|
|
90
|
+
if (!content.endsWith("\n\n")) {
|
|
91
|
+
return content + "\n";
|
|
92
|
+
}
|
|
93
|
+
return content;
|
|
94
|
+
}
|
|
95
|
+
function getProjectRoot() {
|
|
96
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
97
|
+
const __dirname = dirname(__filename);
|
|
98
|
+
let current = __dirname;
|
|
99
|
+
while (current !== dirname(current)) {
|
|
100
|
+
if (existsSync(join(current, "templates"))) {
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
current = dirname(current);
|
|
104
|
+
}
|
|
105
|
+
return join(__dirname, "../..");
|
|
106
|
+
}
|
|
107
|
+
async function generateProblemFiles(problem, language = "python") {
|
|
108
|
+
const problemDir = getSolvingDirPath(problem.id, process.cwd(), problem);
|
|
109
|
+
await mkdir(problemDir, { recursive: true });
|
|
110
|
+
const langConfig = getLanguageConfig(language);
|
|
111
|
+
const projectRoot = getProjectRoot();
|
|
112
|
+
const templatePath = join(projectRoot, "templates", langConfig.templateFile);
|
|
113
|
+
const solutionPath = join(problemDir, `solution.${langConfig.extension}`);
|
|
114
|
+
try {
|
|
115
|
+
const templateContent = await readFile(templatePath, "utf-8");
|
|
116
|
+
await writeFile(solutionPath, templateContent, "utf-8");
|
|
117
|
+
} catch {
|
|
118
|
+
await writeFile(
|
|
119
|
+
solutionPath,
|
|
120
|
+
`// Problem ${problem.id}: ${problem.title}
|
|
121
|
+
`,
|
|
122
|
+
"utf-8"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const testcasesDir = join(problemDir, "testcases");
|
|
126
|
+
for (let i = 0; i < problem.testCases.length; i++) {
|
|
127
|
+
const testCase = problem.testCases[i];
|
|
128
|
+
const caseDir = join(testcasesDir, String(i + 1));
|
|
129
|
+
await mkdir(caseDir, { recursive: true });
|
|
130
|
+
const inputPath = join(caseDir, "input.txt");
|
|
131
|
+
const outputPath = join(caseDir, "output.txt");
|
|
132
|
+
await writeFile(inputPath, testCase.input, "utf-8");
|
|
133
|
+
await writeFile(outputPath, testCase.output, "utf-8");
|
|
134
|
+
}
|
|
135
|
+
const tierName = getTierName(problem.level);
|
|
136
|
+
const tierImageUrl = getTierImageUrl(problem.level);
|
|
137
|
+
const tags = problem.tags.length > 0 ? problem.tags.join(", ") : "\uC5C6\uC74C";
|
|
138
|
+
const includeTag = getIncludeTag();
|
|
139
|
+
const headers = [];
|
|
140
|
+
const values = [];
|
|
141
|
+
headers.push("\uB09C\uC774\uB3C4");
|
|
142
|
+
values.push(`<img src="${tierImageUrl}" alt="${tierName}" width="20" />`);
|
|
143
|
+
if (problem.timeLimit) {
|
|
144
|
+
headers.push("\uC2DC\uAC04 \uC81C\uD55C");
|
|
145
|
+
values.push(problem.timeLimit);
|
|
146
|
+
}
|
|
147
|
+
if (problem.memoryLimit) {
|
|
148
|
+
headers.push("\uBA54\uBAA8\uB9AC \uC81C\uD55C");
|
|
149
|
+
values.push(problem.memoryLimit);
|
|
150
|
+
}
|
|
151
|
+
if (problem.submissions) {
|
|
152
|
+
headers.push("\uC81C\uCD9C");
|
|
153
|
+
values.push(problem.submissions);
|
|
154
|
+
}
|
|
155
|
+
if (problem.accepted) {
|
|
156
|
+
headers.push("\uC815\uB2F5");
|
|
157
|
+
values.push(problem.accepted);
|
|
158
|
+
}
|
|
159
|
+
if (problem.acceptedUsers) {
|
|
160
|
+
headers.push("\uB9DE\uD78C \uC0AC\uB78C");
|
|
161
|
+
values.push(problem.acceptedUsers);
|
|
162
|
+
}
|
|
163
|
+
if (problem.acceptedRate) {
|
|
164
|
+
headers.push("\uC815\uB2F5 \uBE44\uC728");
|
|
165
|
+
values.push(problem.acceptedRate);
|
|
166
|
+
}
|
|
167
|
+
let infoTable = "";
|
|
168
|
+
if (headers.length > 0) {
|
|
169
|
+
const headerRow = `| ${headers.join(" | ")} |`;
|
|
170
|
+
const separatorRow = `|${headers.map(() => "---").join("|")}|`;
|
|
171
|
+
const valueRow = `| ${values.join(" | ")} |`;
|
|
172
|
+
infoTable = `
|
|
173
|
+
${headerRow}
|
|
174
|
+
${separatorRow}
|
|
175
|
+
${valueRow}
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
const description = ensureTrailingNewline(problem.description || "\uC124\uBA85 \uC5C6\uC74C");
|
|
179
|
+
const inputFormat = ensureTrailingNewline(
|
|
180
|
+
problem.inputFormat || "\uC785\uB825 \uD615\uC2DD \uC5C6\uC74C"
|
|
181
|
+
);
|
|
182
|
+
const outputFormat = ensureTrailingNewline(
|
|
183
|
+
problem.outputFormat || "\uCD9C\uB825 \uD615\uC2DD \uC5C6\uC74C"
|
|
184
|
+
);
|
|
185
|
+
const readmeContent = `
|
|
186
|
+
# [${problem.id}: ${problem.title}](https://www.acmicpc.net/problem/${problem.id})
|
|
187
|
+
|
|
188
|
+
${infoTable.trim()}
|
|
189
|
+
|
|
190
|
+
## \uBB38\uC81C \uC124\uBA85
|
|
191
|
+
|
|
192
|
+
${description.trim()}
|
|
193
|
+
|
|
194
|
+
## \uC785\uB825
|
|
195
|
+
|
|
196
|
+
${inputFormat.trim()}
|
|
197
|
+
|
|
198
|
+
## \uCD9C\uB825
|
|
199
|
+
|
|
200
|
+
${outputFormat.trim()}
|
|
201
|
+
|
|
202
|
+
## \uC608\uC81C
|
|
203
|
+
|
|
204
|
+
${problem.testCases.map(
|
|
205
|
+
(tc, i) => `
|
|
206
|
+
### \uC608\uC81C ${i + 1}
|
|
207
|
+
|
|
208
|
+
**\uC785\uB825:**
|
|
209
|
+
|
|
210
|
+
\`\`\`text
|
|
211
|
+
${tc.input.trim()}
|
|
212
|
+
\`\`\`
|
|
213
|
+
|
|
214
|
+
**\uCD9C\uB825:**
|
|
215
|
+
|
|
216
|
+
\`\`\`text
|
|
217
|
+
${tc.output.trim()}
|
|
218
|
+
\`\`\`
|
|
219
|
+
`.trim()
|
|
220
|
+
).join("\n\n")}
|
|
221
|
+
${includeTag ? `
|
|
222
|
+
## \uD0DC\uADF8
|
|
223
|
+
|
|
224
|
+
${tags.trim()}
|
|
225
|
+
` : ""}`.trim() + "\n";
|
|
226
|
+
const readmePath = join(problemDir, "README.md");
|
|
227
|
+
await writeFile(readmePath, readmeContent, "utf-8");
|
|
228
|
+
const meta = {
|
|
229
|
+
id: problem.id,
|
|
230
|
+
title: problem.title,
|
|
231
|
+
level: problem.level,
|
|
232
|
+
tags: problem.tags,
|
|
233
|
+
timeLimit: problem.timeLimit,
|
|
234
|
+
timeLimitMs: parseTimeLimitToMs(problem.timeLimit),
|
|
235
|
+
memoryLimit: problem.memoryLimit
|
|
236
|
+
};
|
|
237
|
+
const metaPath = join(problemDir, "meta.json");
|
|
238
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2), "utf-8");
|
|
239
|
+
return problemDir;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/hooks/use-fetch-problem.ts
|
|
243
|
+
function useFetchProblem({
|
|
244
|
+
problemId,
|
|
245
|
+
language,
|
|
246
|
+
onComplete
|
|
247
|
+
}) {
|
|
248
|
+
const [status, setStatus] = useState(
|
|
249
|
+
"loading"
|
|
250
|
+
);
|
|
251
|
+
const [problem, setProblem] = useState(null);
|
|
252
|
+
const [error, setError] = useState(null);
|
|
253
|
+
const [message, setMessage] = useState("\uBB38\uC81C \uC815\uBCF4\uB97C \uAC00\uC838\uC624\uB294 \uC911...");
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
async function fetchProblem() {
|
|
256
|
+
try {
|
|
257
|
+
setMessage("Solved.ac\uC5D0\uC11C \uBB38\uC81C \uC815\uBCF4\uB97C \uAC00\uC838\uC624\uB294 \uC911...");
|
|
258
|
+
const solvedAcData = await getProblem(problemId);
|
|
259
|
+
setMessage("BOJ\uC5D0\uC11C \uBB38\uC81C \uC0C1\uC138 \uC815\uBCF4\uB97C \uAC00\uC838\uC624\uB294 \uC911...");
|
|
260
|
+
const scrapedData = await scrapeProblem(problemId);
|
|
261
|
+
if (!scrapedData.title && !solvedAcData.titleKo) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`\uBB38\uC81C ${problemId}\uC758 \uC81C\uBAA9\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBB38\uC81C\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const combinedProblem = {
|
|
267
|
+
id: problemId,
|
|
268
|
+
title: solvedAcData.titleKo || scrapedData.title,
|
|
269
|
+
level: solvedAcData.level,
|
|
270
|
+
tier: getTierName(solvedAcData.level),
|
|
271
|
+
tags: solvedAcData.tags.map(
|
|
272
|
+
(tag) => tag.displayNames.find((d) => d.language === "ko")?.name || tag.displayNames[0]?.name || tag.key
|
|
273
|
+
),
|
|
274
|
+
timeLimit: scrapedData.timeLimit,
|
|
275
|
+
memoryLimit: scrapedData.memoryLimit,
|
|
276
|
+
submissions: scrapedData.submissions,
|
|
277
|
+
accepted: scrapedData.accepted,
|
|
278
|
+
acceptedUsers: scrapedData.acceptedUsers,
|
|
279
|
+
acceptedRate: scrapedData.acceptedRate,
|
|
280
|
+
description: scrapedData.description,
|
|
281
|
+
inputFormat: scrapedData.inputFormat,
|
|
282
|
+
outputFormat: scrapedData.outputFormat,
|
|
283
|
+
testCases: scrapedData.testCases
|
|
284
|
+
};
|
|
285
|
+
setProblem(combinedProblem);
|
|
286
|
+
setMessage("\uD30C\uC77C\uC744 \uC0DD\uC131\uD558\uB294 \uC911...");
|
|
287
|
+
const problemDir = await generateProblemFiles(
|
|
288
|
+
combinedProblem,
|
|
289
|
+
language
|
|
290
|
+
);
|
|
291
|
+
setStatus("success");
|
|
292
|
+
setMessage(`\uBB38\uC81C \uD30C\uC77C\uC774 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${problemDir}`);
|
|
293
|
+
if (getAutoOpenEditor()) {
|
|
294
|
+
try {
|
|
295
|
+
const editor = getEditor();
|
|
296
|
+
await execaCommand(`${editor} ${problemDir}`, {
|
|
297
|
+
shell: true,
|
|
298
|
+
detached: true,
|
|
299
|
+
stdio: "ignore"
|
|
300
|
+
});
|
|
301
|
+
setMessage(
|
|
302
|
+
`\uBB38\uC81C \uD30C\uC77C\uC774 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${problemDir}
|
|
303
|
+
${editor}\uB85C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4.`
|
|
304
|
+
);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.warn(
|
|
307
|
+
`\uC5D0\uB514\uD130\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${err instanceof Error ? err.message : String(err)}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
onComplete?.();
|
|
313
|
+
}, 2e3);
|
|
314
|
+
} catch (err) {
|
|
315
|
+
setStatus("error");
|
|
316
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
onComplete?.();
|
|
319
|
+
}, 2e3);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
void fetchProblem();
|
|
323
|
+
}, [problemId, language, onComplete]);
|
|
324
|
+
return {
|
|
325
|
+
status,
|
|
326
|
+
problem,
|
|
327
|
+
error,
|
|
328
|
+
message
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/commands/fetch.tsx
|
|
333
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
334
|
+
var fetchFlagsSchema = {
|
|
335
|
+
language: {
|
|
336
|
+
type: "string",
|
|
337
|
+
shortFlag: "l",
|
|
338
|
+
description: `\uC5B8\uC5B4 \uC120\uD0DD (${getSupportedLanguagesString()})
|
|
339
|
+
\uAE30\uBCF8\uAC12: python`
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
function FetchView({
|
|
343
|
+
problemId,
|
|
344
|
+
language = "python",
|
|
345
|
+
onComplete
|
|
346
|
+
}) {
|
|
347
|
+
const { status, problem, error, message } = useFetchProblem({
|
|
348
|
+
problemId,
|
|
349
|
+
language,
|
|
350
|
+
onComplete
|
|
351
|
+
});
|
|
352
|
+
if (status === "loading") {
|
|
353
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
354
|
+
/* @__PURE__ */ jsx2(Spinner, { label: message }),
|
|
355
|
+
problem && /* @__PURE__ */ jsx2(ProblemDashboard, { problem })
|
|
356
|
+
] });
|
|
357
|
+
}
|
|
358
|
+
if (status === "error") {
|
|
359
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(Alert, { variant: "error", children: [
|
|
360
|
+
"\uC624\uB958 \uBC1C\uC0DD: ",
|
|
361
|
+
error
|
|
362
|
+
] }) });
|
|
363
|
+
}
|
|
364
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: "100%", children: [
|
|
365
|
+
problem && /* @__PURE__ */ jsx2(Box2, { alignSelf: "flex-start", children: /* @__PURE__ */ jsx2(ProblemDashboard, { problem }) }),
|
|
366
|
+
/* @__PURE__ */ jsx2(StatusMessage, { variant: "success", children: message })
|
|
367
|
+
] });
|
|
368
|
+
}
|
|
369
|
+
var FetchCommand = class extends Command {
|
|
370
|
+
async execute(args, flags) {
|
|
371
|
+
const context = await resolveProblemContext(args, { requireId: true });
|
|
372
|
+
if (context.problemId === null) {
|
|
373
|
+
logger.error("\uBB38\uC81C \uBC88\uD638\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
|
|
374
|
+
console.log(`\uB3C4\uC6C0\uB9D0: ps fetch --help`);
|
|
375
|
+
process.exit(1);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const validLanguages = getSupportedLanguages();
|
|
379
|
+
const language = flags.language;
|
|
380
|
+
if (language && !validLanguages.includes(language)) {
|
|
381
|
+
console.error(
|
|
382
|
+
`\uC624\uB958: \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC5B8\uC5B4\uC785\uB2C8\uB2E4. (${getSupportedLanguagesString()})`
|
|
383
|
+
);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
await this.renderView(FetchView, {
|
|
388
|
+
problemId: context.problemId,
|
|
389
|
+
language: language || "python"
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
FetchCommand = __decorateClass([
|
|
394
|
+
CommandDef({
|
|
395
|
+
name: "fetch",
|
|
396
|
+
description: `\uBC31\uC900 \uBB38\uC81C\uB97C \uAC00\uC838\uC640\uC11C \uB85C\uCEEC\uC5D0 \uD30C\uC77C\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4.
|
|
397
|
+
- Solved.ac API\uC640 BOJ \uD06C\uB864\uB9C1\uC744 \uD1B5\uD574 \uBB38\uC81C \uC815\uBCF4 \uC218\uC9D1
|
|
398
|
+
- \uBB38\uC81C \uC124\uBA85, \uC785\uCD9C\uB825 \uD615\uC2DD, \uC608\uC81C \uC785\uCD9C\uB825 \uD30C\uC77C \uC790\uB3D9 \uC0DD\uC131
|
|
399
|
+
- \uC120\uD0DD\uD55C \uC5B8\uC5B4\uC758 \uC194\uB8E8\uC158 \uD15C\uD50C\uB9BF \uD30C\uC77C \uC0DD\uC131
|
|
400
|
+
- README.md\uC5D0 \uBB38\uC81C \uC815\uBCF4, \uD1B5\uACC4, \uD0DC\uADF8(\uC124\uC815 \uC2DC) \uB4F1 \uD3EC\uD568
|
|
401
|
+
- \uAE30\uBCF8 \uC5B8\uC5B4, \uC5D0\uB514\uD130 \uC124\uC815 \uB4F1\uC740 ps config\uC5D0\uC11C \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.`,
|
|
402
|
+
flags: defineFlags(fetchFlagsSchema),
|
|
403
|
+
autoDetectProblemId: false,
|
|
404
|
+
requireProblemId: true,
|
|
405
|
+
examples: ["fetch 1000", "fetch 1000 --language python", "fetch 1000 -l cpp"]
|
|
406
|
+
})
|
|
407
|
+
], FetchCommand);
|
|
408
|
+
var fetch_default = CommandBuilder.fromClass(FetchCommand);
|
|
409
|
+
|
|
410
|
+
export {
|
|
411
|
+
FetchView,
|
|
412
|
+
FetchCommand,
|
|
413
|
+
fetch_default
|
|
414
|
+
};
|
|
@@ -1,7 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
logger
|
|
4
|
+
} from "./chunk-Q5NECGFA.js";
|
|
2
5
|
|
|
3
6
|
// src/services/scraper.ts
|
|
4
7
|
import * as cheerio from "cheerio";
|
|
8
|
+
|
|
9
|
+
// src/utils/http.ts
|
|
10
|
+
var MAX_RETRIES = 3;
|
|
11
|
+
var RETRY_DELAY = 1e3;
|
|
12
|
+
var DEFAULT_HEADERS = {
|
|
13
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
14
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
15
|
+
"Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"
|
|
16
|
+
};
|
|
17
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
async function fetchWithRetry(url, context) {
|
|
19
|
+
let lastError = null;
|
|
20
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(url, {
|
|
23
|
+
headers: DEFAULT_HEADERS
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
if (response.status === 403 || response.status === 429) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`\uBC31\uC900 \uC0AC\uC774\uD2B8 \uC811\uADFC\uC774 \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (HTTP ${response.status}). \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
throw new Error(
|
|
32
|
+
`\uD398\uC774\uC9C0\uB97C \uAC00\uC838\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4: HTTP ${response.status}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const html = await response.text();
|
|
36
|
+
if (!html || html.trim().length === 0) {
|
|
37
|
+
throw new Error("\uC751\uB2F5 \uBCF8\uBB38\uC774 \uBE44\uC5B4\uC788\uC2B5\uB2C8\uB2E4.");
|
|
38
|
+
}
|
|
39
|
+
if (html.includes("Access Denied") || html.includes("CAPTCHA")) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"\uBC31\uC900\uC758 \uBD07 \uBC29\uC9C0 \uC2DC\uC2A4\uD15C\uC5D0 \uC758\uD574 \uC811\uADFC\uC774 \uCC28\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC9C1\uC811 \uC811\uC18D\uD558\uC5EC \uD655\uC778\uC774 \uD544\uC694\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return html;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
lastError = error;
|
|
47
|
+
if (attempt < MAX_RETRIES) {
|
|
48
|
+
const delay = RETRY_DELAY * Math.pow(2, attempt - 1);
|
|
49
|
+
logger.warn(
|
|
50
|
+
`${context} \uB370\uC774\uD130\uB97C \uAC00\uC838\uC624\uB294 \uB370 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (${attempt}/${MAX_RETRIES}). ${delay}ms \uD6C4 \uC7AC\uC2DC\uB3C4\uD569\uB2C8\uB2E4...`
|
|
51
|
+
);
|
|
52
|
+
await sleep(delay);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
throw lastError || new Error(`${context} \uB370\uC774\uD130\uB97C \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/services/scraper.ts
|
|
5
60
|
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
6
61
|
var SOLVED_AC_BASE_URL = "https://solved.ac";
|
|
7
62
|
function htmlToMarkdown($, element) {
|
|
@@ -48,7 +103,7 @@ function htmlToMarkdown($, element) {
|
|
|
48
103
|
case "div": {
|
|
49
104
|
const divContent = htmlToMarkdown($, $node);
|
|
50
105
|
if (divContent) {
|
|
51
|
-
result += divContent + "\n";
|
|
106
|
+
result += divContent + "\n\n";
|
|
52
107
|
}
|
|
53
108
|
break;
|
|
54
109
|
}
|
|
@@ -65,6 +120,7 @@ function htmlToMarkdown($, element) {
|
|
|
65
120
|
\`\`\`
|
|
66
121
|
${preContent}
|
|
67
122
|
\`\`\`
|
|
123
|
+
|
|
68
124
|
`;
|
|
69
125
|
}
|
|
70
126
|
break;
|
|
@@ -78,13 +134,14 @@ ${preContent}
|
|
|
78
134
|
`;
|
|
79
135
|
}
|
|
80
136
|
});
|
|
137
|
+
result += "\n";
|
|
81
138
|
break;
|
|
82
139
|
case "li":
|
|
83
140
|
result += htmlToMarkdown($, $node);
|
|
84
141
|
break;
|
|
85
142
|
case "img": {
|
|
86
143
|
const imgSrc = $node.attr("src") || "";
|
|
87
|
-
const imgAlt = $node.attr("alt") || "";
|
|
144
|
+
const imgAlt = $node.attr("alt") || "\uC774\uBBF8\uC9C0";
|
|
88
145
|
if (imgSrc) {
|
|
89
146
|
let imageUrl = imgSrc;
|
|
90
147
|
if (imgSrc.startsWith("/")) {
|
|
@@ -92,7 +149,9 @@ ${preContent}
|
|
|
92
149
|
} else if (!imgSrc.startsWith("http") && !imgSrc.startsWith("data:")) {
|
|
93
150
|
imageUrl = `${BOJ_BASE_URL}/${imgSrc}`;
|
|
94
151
|
}
|
|
95
|
-
result += `
|
|
152
|
+
result += `
|
|
153
|
+
|
|
154
|
+
`;
|
|
96
155
|
}
|
|
97
156
|
break;
|
|
98
157
|
}
|
|
@@ -105,15 +164,7 @@ ${preContent}
|
|
|
105
164
|
}
|
|
106
165
|
async function scrapeProblem(problemId) {
|
|
107
166
|
const url = `${BOJ_BASE_URL}/problem/${problemId}`;
|
|
108
|
-
const
|
|
109
|
-
headers: {
|
|
110
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
throw new Error(`Failed to fetch problem page: HTTP ${response.status}`);
|
|
115
|
-
}
|
|
116
|
-
const html = await response.text();
|
|
167
|
+
const html = await fetchWithRetry(url, `\uBB38\uC81C ${problemId}`);
|
|
117
168
|
const $ = cheerio.load(html);
|
|
118
169
|
const title = $("#problem_title").text().trim();
|
|
119
170
|
const descriptionEl = $("#problem_description");
|
|
@@ -224,12 +275,12 @@ async function scrapeProblem(problemId) {
|
|
|
224
275
|
}
|
|
225
276
|
if (!title) {
|
|
226
277
|
throw new Error(
|
|
227
|
-
`\uBB38\uC81C ${problemId}\uC758 \uC81C\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. BOJ \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uAC70\uB098 \uBB38\uC81C\
|
|
278
|
+
`\uBB38\uC81C ${problemId}\uC758 \uC81C\uBAA9\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. BOJ \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uAC70\uB098, \uB85C\uADF8\uC778\uC774 \uD544\uC694\uD55C \uBB38\uC81C\uC774\uAC70\uB098, \uC874\uC7AC\uD558\uC9C0 \uC54A\uB294 \uBB38\uC81C\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
|
|
228
279
|
);
|
|
229
280
|
}
|
|
230
281
|
if (!description && !inputFormat && !outputFormat) {
|
|
231
282
|
throw new Error(
|
|
232
|
-
`\uBB38\uC81C ${problemId}\uC758 \uB0B4\uC6A9\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
|
|
283
|
+
`\uBB38\uC81C ${problemId}\uC758 \uB0B4\uC6A9\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBC31\uC900\uC758 \uBD07 \uBC29\uC9C0 \uC2DC\uC2A4\uD15C\uC5D0 \uC758\uD574 \uCC28\uB2E8\uB418\uC5C8\uAC70\uB098, \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
|
|
233
284
|
);
|
|
234
285
|
}
|
|
235
286
|
return {
|
|
@@ -375,8 +426,115 @@ async function searchProblems(query, page = 1) {
|
|
|
375
426
|
totalPages
|
|
376
427
|
};
|
|
377
428
|
}
|
|
429
|
+
async function scrapeUserStats(handle) {
|
|
430
|
+
const url = `${BOJ_BASE_URL}/user/${encodeURIComponent(handle)}`;
|
|
431
|
+
const html = await fetchWithRetry(url, `\uC0AC\uC6A9\uC790 ${handle}`);
|
|
432
|
+
const $ = cheerio.load(html);
|
|
433
|
+
const stats = {
|
|
434
|
+
submissions: 0,
|
|
435
|
+
accepted: 0,
|
|
436
|
+
wrong: 0,
|
|
437
|
+
timeout: 0,
|
|
438
|
+
memory: 0,
|
|
439
|
+
runtimeError: 0,
|
|
440
|
+
compileError: 0
|
|
441
|
+
};
|
|
442
|
+
const statTable = $("#stat-table");
|
|
443
|
+
if (statTable.length > 0) {
|
|
444
|
+
statTable.find("tr").each((_, row) => {
|
|
445
|
+
const th = $(row).find("th").text().trim();
|
|
446
|
+
const td = $(row).find("td").text().trim();
|
|
447
|
+
const value = parseInt(td.replace(/,/g, ""), 10) || 0;
|
|
448
|
+
if (th.includes("\uC81C\uCD9C")) stats.submissions = value;
|
|
449
|
+
else if (th.includes("\uC815\uB2F5")) stats.accepted = value;
|
|
450
|
+
else if (th.includes("\uD2C0\uB9B0 \uACB0\uACFC")) stats.wrong = value;
|
|
451
|
+
else if (th.includes("\uC2DC\uAC04 \uCD08\uACFC")) stats.timeout = value;
|
|
452
|
+
else if (th.includes("\uBA54\uBAA8\uB9AC \uCD08\uACFC")) stats.memory = value;
|
|
453
|
+
else if (th.includes("\uB7F0\uD0C0\uC784 \uC5D0\uB7EC")) stats.runtimeError = value;
|
|
454
|
+
else if (th.includes("\uCEF4\uD30C\uC77C \uC5D0\uB7EC")) stats.compileError = value;
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return stats;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/services/solved-api.ts
|
|
461
|
+
var BASE_URL = "https://solved.ac/api/v3";
|
|
462
|
+
var USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
463
|
+
async function fetchWithRetry2(url, options = {}, maxRetries = 3) {
|
|
464
|
+
let lastError = null;
|
|
465
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
466
|
+
try {
|
|
467
|
+
const response = await fetch(url, {
|
|
468
|
+
...options,
|
|
469
|
+
headers: {
|
|
470
|
+
"User-Agent": USER_AGENT,
|
|
471
|
+
...options.headers
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
if (response.status === 429) {
|
|
475
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
476
|
+
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
|
|
477
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (!response.ok) {
|
|
481
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
482
|
+
}
|
|
483
|
+
return response;
|
|
484
|
+
} catch (error) {
|
|
485
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
486
|
+
if (attempt < maxRetries - 1) {
|
|
487
|
+
await new Promise(
|
|
488
|
+
(resolve) => setTimeout(resolve, (attempt + 1) * 1e3)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
throw lastError || new Error("Request failed after retries");
|
|
494
|
+
}
|
|
495
|
+
async function getProblem(problemId) {
|
|
496
|
+
const url = `${BASE_URL}/problem/show?problemId=${problemId}`;
|
|
497
|
+
const response = await fetchWithRetry2(url);
|
|
498
|
+
const data = await response.json();
|
|
499
|
+
return data;
|
|
500
|
+
}
|
|
501
|
+
async function getUserStats(handle) {
|
|
502
|
+
const url = `${BASE_URL}/user/show?handle=${encodeURIComponent(handle)}`;
|
|
503
|
+
const response = await fetchWithRetry2(url);
|
|
504
|
+
const data = await response.json();
|
|
505
|
+
return data;
|
|
506
|
+
}
|
|
507
|
+
async function getUserTop100(handle) {
|
|
508
|
+
const url = `${BASE_URL}/user/top_100?handle=${encodeURIComponent(handle)}`;
|
|
509
|
+
const response = await fetchWithRetry2(url);
|
|
510
|
+
const data = await response.json();
|
|
511
|
+
return data.items || [];
|
|
512
|
+
}
|
|
513
|
+
async function getUserProblemStats(handle) {
|
|
514
|
+
const url = `${BASE_URL}/user/problem_stats?handle=${encodeURIComponent(
|
|
515
|
+
handle
|
|
516
|
+
)}`;
|
|
517
|
+
const response = await fetchWithRetry2(url);
|
|
518
|
+
const data = await response.json();
|
|
519
|
+
return data;
|
|
520
|
+
}
|
|
521
|
+
async function getUserTagRatings(handle) {
|
|
522
|
+
const url = `${BASE_URL}/user/tag_ratings?handle=${encodeURIComponent(
|
|
523
|
+
handle
|
|
524
|
+
)}`;
|
|
525
|
+
const response = await fetchWithRetry2(url);
|
|
526
|
+
const data = await response.json();
|
|
527
|
+
return data.items || [];
|
|
528
|
+
}
|
|
378
529
|
|
|
379
530
|
export {
|
|
531
|
+
fetchWithRetry,
|
|
380
532
|
scrapeProblem,
|
|
381
|
-
searchProblems
|
|
533
|
+
searchProblems,
|
|
534
|
+
scrapeUserStats,
|
|
535
|
+
getProblem,
|
|
536
|
+
getUserStats,
|
|
537
|
+
getUserTop100,
|
|
538
|
+
getUserProblemStats,
|
|
539
|
+
getUserTagRatings
|
|
382
540
|
};
|