@rhseung/ps-cli 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-A6STXEAE.js +54 -0
- package/dist/chunk-AG6KWWHS.js +342 -0
- package/dist/chunk-GCOFFYJ3.js +47 -0
- package/dist/{chunk-NB4OIWND.js → chunk-HDNNR5OY.js} +111 -51
- package/dist/commands/fetch.js +14 -253
- package/dist/commands/open.js +3 -43
- package/dist/commands/search.js +246 -0
- package/dist/commands/stats.js +28 -14
- package/package.json +2 -2
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/services/solved-api.ts
|
|
4
|
+
var BASE_URL = "https://solved.ac/api/v3";
|
|
5
|
+
var USER_AGENT = "ps-cli/1.0.0";
|
|
6
|
+
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
|
|
7
|
+
let lastError = null;
|
|
8
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(url, {
|
|
11
|
+
...options,
|
|
12
|
+
headers: {
|
|
13
|
+
"User-Agent": USER_AGENT,
|
|
14
|
+
...options.headers
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
if (response.status === 429) {
|
|
18
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
19
|
+
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
return response;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
if (attempt < maxRetries - 1) {
|
|
30
|
+
await new Promise(
|
|
31
|
+
(resolve) => setTimeout(resolve, (attempt + 1) * 1e3)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw lastError || new Error("Request failed after retries");
|
|
37
|
+
}
|
|
38
|
+
async function getProblem(problemId) {
|
|
39
|
+
const url = `${BASE_URL}/problem/show?problemId=${problemId}`;
|
|
40
|
+
const response = await fetchWithRetry(url);
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
async function getUserStats(handle) {
|
|
45
|
+
const url = `${BASE_URL}/user/show?handle=${handle}`;
|
|
46
|
+
const response = await fetchWithRetry(url);
|
|
47
|
+
const data = await response.json();
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
getProblem,
|
|
53
|
+
getUserStats
|
|
54
|
+
};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/services/scraper.ts
|
|
4
|
+
import * as cheerio from "cheerio";
|
|
5
|
+
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
6
|
+
var SOLVED_AC_BASE_URL = "https://solved.ac";
|
|
7
|
+
function htmlToMarkdown($, element) {
|
|
8
|
+
if (element.length === 0) return "";
|
|
9
|
+
let result = "";
|
|
10
|
+
const contents = element.contents();
|
|
11
|
+
if (contents.length === 0) {
|
|
12
|
+
return element.text().trim();
|
|
13
|
+
}
|
|
14
|
+
contents.each((_, node) => {
|
|
15
|
+
if (node.type === "text") {
|
|
16
|
+
const text = node.data || "";
|
|
17
|
+
if (text.trim()) {
|
|
18
|
+
result += text;
|
|
19
|
+
}
|
|
20
|
+
} else if (node.type === "tag") {
|
|
21
|
+
const tagName = node.name.toLowerCase();
|
|
22
|
+
const $node = $(node);
|
|
23
|
+
switch (tagName) {
|
|
24
|
+
case "sup":
|
|
25
|
+
result += `^${htmlToMarkdown($, $node)}`;
|
|
26
|
+
break;
|
|
27
|
+
case "sub":
|
|
28
|
+
result += `<sub>${htmlToMarkdown($, $node)}</sub>`;
|
|
29
|
+
break;
|
|
30
|
+
case "strong":
|
|
31
|
+
case "b":
|
|
32
|
+
result += `**${htmlToMarkdown($, $node)}**`;
|
|
33
|
+
break;
|
|
34
|
+
case "em":
|
|
35
|
+
case "i":
|
|
36
|
+
result += `*${htmlToMarkdown($, $node)}*`;
|
|
37
|
+
break;
|
|
38
|
+
case "br":
|
|
39
|
+
result += "\n";
|
|
40
|
+
break;
|
|
41
|
+
case "p": {
|
|
42
|
+
const pContent = htmlToMarkdown($, $node);
|
|
43
|
+
if (pContent) {
|
|
44
|
+
result += pContent + "\n\n";
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "div": {
|
|
49
|
+
const divContent = htmlToMarkdown($, $node);
|
|
50
|
+
if (divContent) {
|
|
51
|
+
result += divContent + "\n";
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "span":
|
|
56
|
+
result += htmlToMarkdown($, $node);
|
|
57
|
+
break;
|
|
58
|
+
case "code":
|
|
59
|
+
result += `\`${htmlToMarkdown($, $node)}\``;
|
|
60
|
+
break;
|
|
61
|
+
case "pre": {
|
|
62
|
+
const preContent = htmlToMarkdown($, $node);
|
|
63
|
+
if (preContent) {
|
|
64
|
+
result += `
|
|
65
|
+
\`\`\`
|
|
66
|
+
${preContent}
|
|
67
|
+
\`\`\`
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "ul":
|
|
73
|
+
case "ol":
|
|
74
|
+
$node.find("li").each((i, li) => {
|
|
75
|
+
const liContent = htmlToMarkdown($, $(li));
|
|
76
|
+
if (liContent) {
|
|
77
|
+
result += `- ${liContent}
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
case "li":
|
|
83
|
+
result += htmlToMarkdown($, $node);
|
|
84
|
+
break;
|
|
85
|
+
case "img": {
|
|
86
|
+
const imgSrc = $node.attr("src") || "";
|
|
87
|
+
const imgAlt = $node.attr("alt") || "";
|
|
88
|
+
if (imgSrc) {
|
|
89
|
+
let imageUrl = imgSrc;
|
|
90
|
+
if (imgSrc.startsWith("/")) {
|
|
91
|
+
imageUrl = `${BOJ_BASE_URL}${imgSrc}`;
|
|
92
|
+
} else if (!imgSrc.startsWith("http") && !imgSrc.startsWith("data:")) {
|
|
93
|
+
imageUrl = `${BOJ_BASE_URL}/${imgSrc}`;
|
|
94
|
+
}
|
|
95
|
+
result += ``;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
result += htmlToMarkdown($, $node);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return result.trim();
|
|
105
|
+
}
|
|
106
|
+
async function scrapeProblem(problemId) {
|
|
107
|
+
const url = `${BOJ_BASE_URL}/problem/${problemId}`;
|
|
108
|
+
const response = await fetch(url, {
|
|
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();
|
|
117
|
+
const $ = cheerio.load(html);
|
|
118
|
+
const title = $("#problem_title").text().trim();
|
|
119
|
+
const descriptionEl = $("#problem_description");
|
|
120
|
+
let description = "";
|
|
121
|
+
if (descriptionEl.length > 0) {
|
|
122
|
+
description = htmlToMarkdown($, descriptionEl).trim();
|
|
123
|
+
if (!description) {
|
|
124
|
+
description = descriptionEl.text().trim();
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
const altDesc = $('[id*="description"]').first();
|
|
128
|
+
if (altDesc.length > 0) {
|
|
129
|
+
description = altDesc.text().trim();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const inputEl = $("#problem_input");
|
|
133
|
+
let inputFormat = "";
|
|
134
|
+
if (inputEl.length > 0) {
|
|
135
|
+
inputFormat = htmlToMarkdown($, inputEl).trim();
|
|
136
|
+
if (!inputFormat) {
|
|
137
|
+
inputFormat = inputEl.text().trim();
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
const altInput = $('[id*="input"]').first();
|
|
141
|
+
if (altInput.length > 0) {
|
|
142
|
+
inputFormat = altInput.text().trim();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const outputEl = $("#problem_output");
|
|
146
|
+
let outputFormat = "";
|
|
147
|
+
if (outputEl.length > 0) {
|
|
148
|
+
outputFormat = htmlToMarkdown($, outputEl).trim();
|
|
149
|
+
if (!outputFormat) {
|
|
150
|
+
outputFormat = outputEl.text().trim();
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
const altOutput = $('[id*="output"]').first();
|
|
154
|
+
if (altOutput.length > 0) {
|
|
155
|
+
outputFormat = altOutput.text().trim();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const problemInfo = {};
|
|
159
|
+
const problemInfoTable = $("#problem-info");
|
|
160
|
+
const tableInResponsive = $(".table-responsive table");
|
|
161
|
+
const targetTable = problemInfoTable.length > 0 ? problemInfoTable : tableInResponsive;
|
|
162
|
+
if (targetTable.length > 0) {
|
|
163
|
+
const headerRow = targetTable.find("thead tr");
|
|
164
|
+
const dataRow = targetTable.find("tbody tr");
|
|
165
|
+
if (headerRow.length > 0 && dataRow.length > 0) {
|
|
166
|
+
const headers = headerRow.find("th").map((_, th) => $(th).text().trim()).get();
|
|
167
|
+
const values = dataRow.find("td").map((_, td) => $(td).text().trim()).get();
|
|
168
|
+
headers.forEach((header, index) => {
|
|
169
|
+
if (values[index]) {
|
|
170
|
+
problemInfo[header] = values[index];
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
targetTable.find("tr").each((_, row) => {
|
|
175
|
+
const tds = $(row).find("td");
|
|
176
|
+
if (tds.length >= 2) {
|
|
177
|
+
const label = $(tds[0]).text().trim();
|
|
178
|
+
const value = $(tds[1]).text().trim();
|
|
179
|
+
problemInfo[label] = value;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const timeLimit = problemInfo["\uC2DC\uAC04 \uC81C\uD55C"] || problemInfo["Time Limit"] || void 0;
|
|
185
|
+
const memoryLimit = problemInfo["\uBA54\uBAA8\uB9AC \uC81C\uD55C"] || problemInfo["Memory Limit"] || void 0;
|
|
186
|
+
const submissions = problemInfo["\uC81C\uCD9C"] || problemInfo["Submit"] || void 0;
|
|
187
|
+
const accepted = problemInfo["\uC815\uB2F5"] || problemInfo["Accepted"] || void 0;
|
|
188
|
+
const acceptedUsers = problemInfo["\uB9DE\uD78C \uC0AC\uB78C"] || problemInfo["Accepted Users"] || void 0;
|
|
189
|
+
const acceptedRate = problemInfo["\uC815\uB2F5 \uBE44\uC728"] || problemInfo["Accepted Rate"] || void 0;
|
|
190
|
+
const testCases = [];
|
|
191
|
+
const sampleInputs = $(".sampledata").filter((_, el) => {
|
|
192
|
+
const id = $(el).attr("id");
|
|
193
|
+
return id?.startsWith("sample-input-") ?? false;
|
|
194
|
+
});
|
|
195
|
+
sampleInputs.each((_, el) => {
|
|
196
|
+
const inputId = $(el).attr("id");
|
|
197
|
+
if (!inputId) return;
|
|
198
|
+
const match = inputId.match(/sample-input-(\d+)/);
|
|
199
|
+
if (!match) return;
|
|
200
|
+
const sampleNumber = match[1];
|
|
201
|
+
const outputId = `sample-output-${sampleNumber}`;
|
|
202
|
+
const outputEl2 = $(`#${outputId}`);
|
|
203
|
+
if (outputEl2.length > 0) {
|
|
204
|
+
testCases.push({
|
|
205
|
+
input: $(el).text(),
|
|
206
|
+
output: outputEl2.text()
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
if (testCases.length === 0) {
|
|
211
|
+
$("pre").each((_, el) => {
|
|
212
|
+
const text = $(el).text().trim();
|
|
213
|
+
const prevText = $(el).prev().text().toLowerCase();
|
|
214
|
+
if (prevText.includes("\uC785\uB825") || prevText.includes("input")) {
|
|
215
|
+
const nextPre = $(el).next("pre");
|
|
216
|
+
if (nextPre.length > 0) {
|
|
217
|
+
testCases.push({
|
|
218
|
+
input: text,
|
|
219
|
+
output: nextPre.text().trim()
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (!title) {
|
|
226
|
+
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\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (!description && !inputFormat && !outputFormat) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`\uBB38\uC81C ${problemId}\uC758 \uB0B4\uC6A9\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. BOJ \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uAC70\uB098 API \uC81C\uD55C\uC5D0 \uAC78\uB838\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
title,
|
|
237
|
+
description,
|
|
238
|
+
inputFormat,
|
|
239
|
+
outputFormat,
|
|
240
|
+
testCases,
|
|
241
|
+
timeLimit,
|
|
242
|
+
memoryLimit,
|
|
243
|
+
submissions,
|
|
244
|
+
accepted,
|
|
245
|
+
acceptedUsers,
|
|
246
|
+
acceptedRate
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
async function searchProblems(query, page = 1) {
|
|
250
|
+
const url = `${SOLVED_AC_BASE_URL}/problems?query=${encodeURIComponent(query)}&page=${page}`;
|
|
251
|
+
const response = await fetch(url, {
|
|
252
|
+
headers: {
|
|
253
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
throw new Error(`Failed to fetch search results: HTTP ${response.status}`);
|
|
258
|
+
}
|
|
259
|
+
const html = await response.text();
|
|
260
|
+
const $ = cheerio.load(html);
|
|
261
|
+
const problems = [];
|
|
262
|
+
const rows = $("tbody tr");
|
|
263
|
+
rows.each((_, row) => {
|
|
264
|
+
const $row = $(row);
|
|
265
|
+
const cells = $row.find("td");
|
|
266
|
+
if (cells.length >= 2) {
|
|
267
|
+
const problemIdText = $(cells[0]).text().trim();
|
|
268
|
+
const problemId = parseInt(problemIdText, 10);
|
|
269
|
+
const title = $(cells[1]).text().trim();
|
|
270
|
+
if (!isNaN(problemId) && title) {
|
|
271
|
+
let level;
|
|
272
|
+
const firstCell = $(cells[0]);
|
|
273
|
+
const tierImg = firstCell.find("img[src*='tier_small']");
|
|
274
|
+
if (tierImg.length > 0) {
|
|
275
|
+
const imgSrc = tierImg.attr("src");
|
|
276
|
+
if (imgSrc) {
|
|
277
|
+
const match = imgSrc.match(/tier_small\/(\d+)\.svg/);
|
|
278
|
+
if (match && match[1]) {
|
|
279
|
+
const parsedLevel = parseInt(match[1], 10);
|
|
280
|
+
if (!isNaN(parsedLevel) && parsedLevel >= 0 && parsedLevel <= 31) {
|
|
281
|
+
level = parsedLevel;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
let solvedCount;
|
|
287
|
+
if (cells.length >= 3) {
|
|
288
|
+
const solvedCountText = $(cells[2]).text().trim();
|
|
289
|
+
const parsed = parseInt(solvedCountText.replace(/,/g, ""), 10);
|
|
290
|
+
if (!isNaN(parsed)) {
|
|
291
|
+
solvedCount = parsed;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let averageTries;
|
|
295
|
+
if (cells.length >= 4) {
|
|
296
|
+
const averageTriesText = $(cells[3]).text().trim();
|
|
297
|
+
const parsed = parseFloat(averageTriesText);
|
|
298
|
+
if (!isNaN(parsed)) {
|
|
299
|
+
averageTries = parsed;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
problems.push({
|
|
303
|
+
problemId,
|
|
304
|
+
title,
|
|
305
|
+
level,
|
|
306
|
+
solvedCount,
|
|
307
|
+
averageTries
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
let totalPages = 1;
|
|
313
|
+
const paginationLinks = $('a[href*="page="]');
|
|
314
|
+
const pageNumbers = [];
|
|
315
|
+
paginationLinks.each((_, link) => {
|
|
316
|
+
const href = $(link).attr("href");
|
|
317
|
+
if (href) {
|
|
318
|
+
const match = href.match(/page=(\d+)/);
|
|
319
|
+
if (match) {
|
|
320
|
+
const pageNum = parseInt(match[1], 10);
|
|
321
|
+
if (!isNaN(pageNum)) {
|
|
322
|
+
pageNumbers.push(pageNum);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
if (pageNumbers.length > 0) {
|
|
328
|
+
totalPages = Math.max(...pageNumbers);
|
|
329
|
+
} else {
|
|
330
|
+
totalPages = problems.length > 0 ? 1 : 0;
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
problems,
|
|
334
|
+
currentPage: page,
|
|
335
|
+
totalPages
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export {
|
|
340
|
+
scrapeProblem,
|
|
341
|
+
searchProblems
|
|
342
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
openBrowser
|
|
4
|
+
} from "./chunk-QGMWUOJ3.js";
|
|
5
|
+
|
|
6
|
+
// src/hooks/use-open-browser.ts
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
9
|
+
function useOpenBrowser({
|
|
10
|
+
problemId,
|
|
11
|
+
onComplete
|
|
12
|
+
}) {
|
|
13
|
+
const [status, setStatus] = useState(
|
|
14
|
+
"loading"
|
|
15
|
+
);
|
|
16
|
+
const [error, setError] = useState(null);
|
|
17
|
+
const [url, setUrl] = useState("");
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
async function handleOpenBrowser() {
|
|
20
|
+
try {
|
|
21
|
+
const problemUrl = `${BOJ_BASE_URL}/problem/${problemId}`;
|
|
22
|
+
setUrl(problemUrl);
|
|
23
|
+
await openBrowser(problemUrl);
|
|
24
|
+
setStatus("success");
|
|
25
|
+
setTimeout(() => {
|
|
26
|
+
onComplete?.();
|
|
27
|
+
}, 1500);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
setStatus("error");
|
|
30
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
onComplete?.();
|
|
33
|
+
}, 2e3);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
void handleOpenBrowser();
|
|
37
|
+
}, [problemId, onComplete]);
|
|
38
|
+
return {
|
|
39
|
+
status,
|
|
40
|
+
error,
|
|
41
|
+
url
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
useOpenBrowser
|
|
47
|
+
};
|
|
@@ -496,6 +496,7 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
496
496
|
var source_default = chalk;
|
|
497
497
|
|
|
498
498
|
// src/utils/tier.ts
|
|
499
|
+
import gradient from "gradient-string";
|
|
499
500
|
var TIER_NAMES = [
|
|
500
501
|
void 0,
|
|
501
502
|
"Bronze V",
|
|
@@ -565,6 +566,110 @@ var TIER_COLORS = [
|
|
|
565
566
|
"#b300e0"
|
|
566
567
|
];
|
|
567
568
|
var TIER_IMAGE_BASE_URL = "https://d2gd6pc034wcta.cloudfront.net/tier";
|
|
569
|
+
var MASTER_TIER_GRADIENT = [
|
|
570
|
+
{ r: 255, g: 124, b: 168 },
|
|
571
|
+
{ r: 180, g: 145, b: 255 },
|
|
572
|
+
{ r: 124, g: 249, b: 255 }
|
|
573
|
+
];
|
|
574
|
+
var TIER_MIN_RATINGS = [
|
|
575
|
+
0,
|
|
576
|
+
// Unrated (tier 0): 0-29
|
|
577
|
+
30,
|
|
578
|
+
// Bronze V (tier 1)
|
|
579
|
+
60,
|
|
580
|
+
// Bronze IV (tier 2)
|
|
581
|
+
90,
|
|
582
|
+
// Bronze III (tier 3)
|
|
583
|
+
120,
|
|
584
|
+
// Bronze II (tier 4)
|
|
585
|
+
150,
|
|
586
|
+
// Bronze I (tier 5)
|
|
587
|
+
200,
|
|
588
|
+
// Silver V (tier 6)
|
|
589
|
+
300,
|
|
590
|
+
// Silver IV (tier 7)
|
|
591
|
+
400,
|
|
592
|
+
// Silver III (tier 8)
|
|
593
|
+
500,
|
|
594
|
+
// Silver II (tier 9)
|
|
595
|
+
650,
|
|
596
|
+
// Silver I (tier 10)
|
|
597
|
+
800,
|
|
598
|
+
// Gold V (tier 11)
|
|
599
|
+
950,
|
|
600
|
+
// Gold IV (tier 12)
|
|
601
|
+
1100,
|
|
602
|
+
// Gold III (tier 13)
|
|
603
|
+
1250,
|
|
604
|
+
// Gold II (tier 14)
|
|
605
|
+
1400,
|
|
606
|
+
// Gold I (tier 15)
|
|
607
|
+
1600,
|
|
608
|
+
// Platinum V (tier 16)
|
|
609
|
+
1750,
|
|
610
|
+
// Platinum IV (tier 17)
|
|
611
|
+
1900,
|
|
612
|
+
// Platinum III (tier 18)
|
|
613
|
+
2e3,
|
|
614
|
+
// Platinum II (tier 19)
|
|
615
|
+
2100,
|
|
616
|
+
// Platinum I (tier 20)
|
|
617
|
+
2200,
|
|
618
|
+
// Diamond V (tier 21)
|
|
619
|
+
2300,
|
|
620
|
+
// Diamond IV (tier 22)
|
|
621
|
+
2400,
|
|
622
|
+
// Diamond III (tier 23)
|
|
623
|
+
2500,
|
|
624
|
+
// Diamond II (tier 24)
|
|
625
|
+
2600,
|
|
626
|
+
// Diamond I (tier 25)
|
|
627
|
+
2700,
|
|
628
|
+
// Ruby V (tier 26)
|
|
629
|
+
2800,
|
|
630
|
+
// Ruby IV (tier 27)
|
|
631
|
+
2850,
|
|
632
|
+
// Ruby III (tier 28)
|
|
633
|
+
2900,
|
|
634
|
+
// Ruby II (tier 29)
|
|
635
|
+
2950,
|
|
636
|
+
// Ruby I (tier 30)
|
|
637
|
+
3e3
|
|
638
|
+
// Master (tier 31)
|
|
639
|
+
];
|
|
640
|
+
function getTierMinRating(tier) {
|
|
641
|
+
if (tier >= 0 && tier < TIER_MIN_RATINGS.length) {
|
|
642
|
+
return TIER_MIN_RATINGS[tier] ?? 0;
|
|
643
|
+
}
|
|
644
|
+
return 0;
|
|
645
|
+
}
|
|
646
|
+
function getNextTierMinRating(tier) {
|
|
647
|
+
if (tier === 31) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
if (tier >= 0 && tier < TIER_MIN_RATINGS.length - 1) {
|
|
651
|
+
return TIER_MIN_RATINGS[tier + 1] ?? null;
|
|
652
|
+
}
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
function calculateTierProgress(currentRating, tier) {
|
|
656
|
+
if (tier === 31) {
|
|
657
|
+
return 100;
|
|
658
|
+
}
|
|
659
|
+
const currentTierMin = getTierMinRating(tier);
|
|
660
|
+
const nextTierMin = getNextTierMinRating(tier);
|
|
661
|
+
if (nextTierMin === null) {
|
|
662
|
+
return 100;
|
|
663
|
+
}
|
|
664
|
+
if (currentRating < currentTierMin) {
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
if (currentRating >= nextTierMin) {
|
|
668
|
+
return 100;
|
|
669
|
+
}
|
|
670
|
+
const progress = (currentRating - currentTierMin) / (nextTierMin - currentTierMin) * 100;
|
|
671
|
+
return Math.max(0, Math.min(100, progress));
|
|
672
|
+
}
|
|
568
673
|
function getTierName(level) {
|
|
569
674
|
if (level === 0) return "Unrated";
|
|
570
675
|
if (level >= 1 && level < TIER_NAMES.length) {
|
|
@@ -574,6 +679,9 @@ function getTierName(level) {
|
|
|
574
679
|
}
|
|
575
680
|
function getTierColor(level) {
|
|
576
681
|
if (level === 0) return "#2d2d2d";
|
|
682
|
+
if (level === 31) {
|
|
683
|
+
return gradient([...MASTER_TIER_GRADIENT]);
|
|
684
|
+
}
|
|
577
685
|
if (level >= 1 && level < TIER_COLORS.length) {
|
|
578
686
|
return TIER_COLORS[level] || "#2d2d2d";
|
|
579
687
|
}
|
|
@@ -583,59 +691,11 @@ function getTierImageUrl(level) {
|
|
|
583
691
|
return `${TIER_IMAGE_BASE_URL}/${level}.svg`;
|
|
584
692
|
}
|
|
585
693
|
|
|
586
|
-
// src/services/solved-api.ts
|
|
587
|
-
var BASE_URL = "https://solved.ac/api/v3";
|
|
588
|
-
var USER_AGENT = "ps-cli/1.0.0";
|
|
589
|
-
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
|
|
590
|
-
let lastError = null;
|
|
591
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
592
|
-
try {
|
|
593
|
-
const response = await fetch(url, {
|
|
594
|
-
...options,
|
|
595
|
-
headers: {
|
|
596
|
-
"User-Agent": USER_AGENT,
|
|
597
|
-
...options.headers
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
if (response.status === 429) {
|
|
601
|
-
const retryAfter = response.headers.get("Retry-After");
|
|
602
|
-
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
|
|
603
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
if (!response.ok) {
|
|
607
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
608
|
-
}
|
|
609
|
-
return response;
|
|
610
|
-
} catch (error) {
|
|
611
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
612
|
-
if (attempt < maxRetries - 1) {
|
|
613
|
-
await new Promise(
|
|
614
|
-
(resolve) => setTimeout(resolve, (attempt + 1) * 1e3)
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
throw lastError || new Error("Request failed after retries");
|
|
620
|
-
}
|
|
621
|
-
async function getProblem(problemId) {
|
|
622
|
-
const url = `${BASE_URL}/problem/show?problemId=${problemId}`;
|
|
623
|
-
const response = await fetchWithRetry(url);
|
|
624
|
-
const data = await response.json();
|
|
625
|
-
return data;
|
|
626
|
-
}
|
|
627
|
-
async function getUserStats(handle) {
|
|
628
|
-
const url = `${BASE_URL}/user/show?handle=${handle}`;
|
|
629
|
-
const response = await fetchWithRetry(url);
|
|
630
|
-
const data = await response.json();
|
|
631
|
-
return data;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
694
|
export {
|
|
635
695
|
source_default,
|
|
696
|
+
getNextTierMinRating,
|
|
697
|
+
calculateTierProgress,
|
|
636
698
|
getTierName,
|
|
637
699
|
getTierColor,
|
|
638
|
-
getTierImageUrl
|
|
639
|
-
getProblem,
|
|
640
|
-
getUserStats
|
|
700
|
+
getTierImageUrl
|
|
641
701
|
};
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
scrapeProblem
|
|
4
|
+
} from "../chunk-AG6KWWHS.js";
|
|
5
|
+
import {
|
|
6
|
+
getProblem
|
|
7
|
+
} from "../chunk-A6STXEAE.js";
|
|
8
|
+
import {
|
|
4
9
|
getTierColor,
|
|
5
10
|
getTierImageUrl,
|
|
6
11
|
getTierName,
|
|
7
12
|
source_default
|
|
8
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-HDNNR5OY.js";
|
|
9
14
|
import {
|
|
10
15
|
Command,
|
|
11
16
|
CommandBuilder,
|
|
@@ -28,21 +33,23 @@ import { Spinner } from "@inkjs/ui";
|
|
|
28
33
|
import { Box as Box2 } from "ink";
|
|
29
34
|
|
|
30
35
|
// src/components/problem-dashboard.tsx
|
|
31
|
-
import { Box, Text } from "ink";
|
|
36
|
+
import { Box, Text, Transform } from "ink";
|
|
32
37
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
33
38
|
function ProblemDashboard({ problem }) {
|
|
34
39
|
const tierName = getTierName(problem.level);
|
|
35
40
|
const tierColor = getTierColor(problem.level);
|
|
36
|
-
|
|
41
|
+
const tierColorFn = typeof tierColor === "string" ? source_default.hex(tierColor) : tierColor.multiline;
|
|
42
|
+
const borderColorString = typeof tierColor === "string" ? tierColor : "#ff7ca8";
|
|
43
|
+
return /* @__PURE__ */ jsx(Transform, { transform: (output) => tierColorFn(output), children: /* @__PURE__ */ jsx(
|
|
37
44
|
Box,
|
|
38
45
|
{
|
|
39
46
|
flexDirection: "column",
|
|
40
47
|
borderStyle: "round",
|
|
41
|
-
borderColor:
|
|
48
|
+
borderColor: borderColorString,
|
|
42
49
|
paddingX: 1,
|
|
43
50
|
alignSelf: "flex-start",
|
|
44
51
|
children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
45
|
-
|
|
52
|
+
tierName,
|
|
46
53
|
" ",
|
|
47
54
|
/* @__PURE__ */ jsxs(Text, { color: "white", children: [
|
|
48
55
|
"#",
|
|
@@ -52,7 +59,7 @@ function ProblemDashboard({ problem }) {
|
|
|
52
59
|
] })
|
|
53
60
|
] })
|
|
54
61
|
}
|
|
55
|
-
);
|
|
62
|
+
) });
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
// src/hooks/use-fetch-problem.ts
|
|
@@ -191,252 +198,6 @@ ${tags}
|
|
|
191
198
|
return problemDir;
|
|
192
199
|
}
|
|
193
200
|
|
|
194
|
-
// src/services/scraper.ts
|
|
195
|
-
import * as cheerio from "cheerio";
|
|
196
|
-
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
197
|
-
function htmlToMarkdown($, element) {
|
|
198
|
-
if (element.length === 0) return "";
|
|
199
|
-
let result = "";
|
|
200
|
-
const contents = element.contents();
|
|
201
|
-
if (contents.length === 0) {
|
|
202
|
-
return element.text().trim();
|
|
203
|
-
}
|
|
204
|
-
contents.each((_, node) => {
|
|
205
|
-
if (node.type === "text") {
|
|
206
|
-
const text = node.data || "";
|
|
207
|
-
if (text.trim()) {
|
|
208
|
-
result += text;
|
|
209
|
-
}
|
|
210
|
-
} else if (node.type === "tag") {
|
|
211
|
-
const tagName = node.name.toLowerCase();
|
|
212
|
-
const $node = $(node);
|
|
213
|
-
switch (tagName) {
|
|
214
|
-
case "sup":
|
|
215
|
-
result += `^${htmlToMarkdown($, $node)}`;
|
|
216
|
-
break;
|
|
217
|
-
case "sub":
|
|
218
|
-
result += `<sub>${htmlToMarkdown($, $node)}</sub>`;
|
|
219
|
-
break;
|
|
220
|
-
case "strong":
|
|
221
|
-
case "b":
|
|
222
|
-
result += `**${htmlToMarkdown($, $node)}**`;
|
|
223
|
-
break;
|
|
224
|
-
case "em":
|
|
225
|
-
case "i":
|
|
226
|
-
result += `*${htmlToMarkdown($, $node)}*`;
|
|
227
|
-
break;
|
|
228
|
-
case "br":
|
|
229
|
-
result += "\n";
|
|
230
|
-
break;
|
|
231
|
-
case "p": {
|
|
232
|
-
const pContent = htmlToMarkdown($, $node);
|
|
233
|
-
if (pContent) {
|
|
234
|
-
result += pContent + "\n\n";
|
|
235
|
-
}
|
|
236
|
-
break;
|
|
237
|
-
}
|
|
238
|
-
case "div": {
|
|
239
|
-
const divContent = htmlToMarkdown($, $node);
|
|
240
|
-
if (divContent) {
|
|
241
|
-
result += divContent + "\n";
|
|
242
|
-
}
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case "span":
|
|
246
|
-
result += htmlToMarkdown($, $node);
|
|
247
|
-
break;
|
|
248
|
-
case "code":
|
|
249
|
-
result += `\`${htmlToMarkdown($, $node)}\``;
|
|
250
|
-
break;
|
|
251
|
-
case "pre": {
|
|
252
|
-
const preContent = htmlToMarkdown($, $node);
|
|
253
|
-
if (preContent) {
|
|
254
|
-
result += `
|
|
255
|
-
\`\`\`
|
|
256
|
-
${preContent}
|
|
257
|
-
\`\`\`
|
|
258
|
-
`;
|
|
259
|
-
}
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
case "ul":
|
|
263
|
-
case "ol":
|
|
264
|
-
$node.find("li").each((i, li) => {
|
|
265
|
-
const liContent = htmlToMarkdown($, $(li));
|
|
266
|
-
if (liContent) {
|
|
267
|
-
result += `- ${liContent}
|
|
268
|
-
`;
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
break;
|
|
272
|
-
case "li":
|
|
273
|
-
result += htmlToMarkdown($, $node);
|
|
274
|
-
break;
|
|
275
|
-
case "img": {
|
|
276
|
-
const imgSrc = $node.attr("src") || "";
|
|
277
|
-
const imgAlt = $node.attr("alt") || "";
|
|
278
|
-
if (imgSrc) {
|
|
279
|
-
let imageUrl = imgSrc;
|
|
280
|
-
if (imgSrc.startsWith("/")) {
|
|
281
|
-
imageUrl = `${BOJ_BASE_URL}${imgSrc}`;
|
|
282
|
-
} else if (!imgSrc.startsWith("http") && !imgSrc.startsWith("data:")) {
|
|
283
|
-
imageUrl = `${BOJ_BASE_URL}/${imgSrc}`;
|
|
284
|
-
}
|
|
285
|
-
result += ``;
|
|
286
|
-
}
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
default:
|
|
290
|
-
result += htmlToMarkdown($, $node);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
return result.trim();
|
|
295
|
-
}
|
|
296
|
-
async function scrapeProblem(problemId) {
|
|
297
|
-
const url = `${BOJ_BASE_URL}/problem/${problemId}`;
|
|
298
|
-
const response = await fetch(url, {
|
|
299
|
-
headers: {
|
|
300
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
if (!response.ok) {
|
|
304
|
-
throw new Error(`Failed to fetch problem page: HTTP ${response.status}`);
|
|
305
|
-
}
|
|
306
|
-
const html = await response.text();
|
|
307
|
-
const $ = cheerio.load(html);
|
|
308
|
-
const title = $("#problem_title").text().trim();
|
|
309
|
-
const descriptionEl = $("#problem_description");
|
|
310
|
-
let description = "";
|
|
311
|
-
if (descriptionEl.length > 0) {
|
|
312
|
-
description = htmlToMarkdown($, descriptionEl).trim();
|
|
313
|
-
if (!description) {
|
|
314
|
-
description = descriptionEl.text().trim();
|
|
315
|
-
}
|
|
316
|
-
} else {
|
|
317
|
-
const altDesc = $('[id*="description"]').first();
|
|
318
|
-
if (altDesc.length > 0) {
|
|
319
|
-
description = altDesc.text().trim();
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const inputEl = $("#problem_input");
|
|
323
|
-
let inputFormat = "";
|
|
324
|
-
if (inputEl.length > 0) {
|
|
325
|
-
inputFormat = htmlToMarkdown($, inputEl).trim();
|
|
326
|
-
if (!inputFormat) {
|
|
327
|
-
inputFormat = inputEl.text().trim();
|
|
328
|
-
}
|
|
329
|
-
} else {
|
|
330
|
-
const altInput = $('[id*="input"]').first();
|
|
331
|
-
if (altInput.length > 0) {
|
|
332
|
-
inputFormat = altInput.text().trim();
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
const outputEl = $("#problem_output");
|
|
336
|
-
let outputFormat = "";
|
|
337
|
-
if (outputEl.length > 0) {
|
|
338
|
-
outputFormat = htmlToMarkdown($, outputEl).trim();
|
|
339
|
-
if (!outputFormat) {
|
|
340
|
-
outputFormat = outputEl.text().trim();
|
|
341
|
-
}
|
|
342
|
-
} else {
|
|
343
|
-
const altOutput = $('[id*="output"]').first();
|
|
344
|
-
if (altOutput.length > 0) {
|
|
345
|
-
outputFormat = altOutput.text().trim();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
const problemInfo = {};
|
|
349
|
-
const problemInfoTable = $("#problem-info");
|
|
350
|
-
const tableInResponsive = $(".table-responsive table");
|
|
351
|
-
const targetTable = problemInfoTable.length > 0 ? problemInfoTable : tableInResponsive;
|
|
352
|
-
if (targetTable.length > 0) {
|
|
353
|
-
const headerRow = targetTable.find("thead tr");
|
|
354
|
-
const dataRow = targetTable.find("tbody tr");
|
|
355
|
-
if (headerRow.length > 0 && dataRow.length > 0) {
|
|
356
|
-
const headers = headerRow.find("th").map((_, th) => $(th).text().trim()).get();
|
|
357
|
-
const values = dataRow.find("td").map((_, td) => $(td).text().trim()).get();
|
|
358
|
-
headers.forEach((header, index) => {
|
|
359
|
-
if (values[index]) {
|
|
360
|
-
problemInfo[header] = values[index];
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
} else {
|
|
364
|
-
targetTable.find("tr").each((_, row) => {
|
|
365
|
-
const tds = $(row).find("td");
|
|
366
|
-
if (tds.length >= 2) {
|
|
367
|
-
const label = $(tds[0]).text().trim();
|
|
368
|
-
const value = $(tds[1]).text().trim();
|
|
369
|
-
problemInfo[label] = value;
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
const timeLimit = problemInfo["\uC2DC\uAC04 \uC81C\uD55C"] || problemInfo["Time Limit"] || void 0;
|
|
375
|
-
const memoryLimit = problemInfo["\uBA54\uBAA8\uB9AC \uC81C\uD55C"] || problemInfo["Memory Limit"] || void 0;
|
|
376
|
-
const submissions = problemInfo["\uC81C\uCD9C"] || problemInfo["Submit"] || void 0;
|
|
377
|
-
const accepted = problemInfo["\uC815\uB2F5"] || problemInfo["Accepted"] || void 0;
|
|
378
|
-
const acceptedUsers = problemInfo["\uB9DE\uD78C \uC0AC\uB78C"] || problemInfo["Accepted Users"] || void 0;
|
|
379
|
-
const acceptedRate = problemInfo["\uC815\uB2F5 \uBE44\uC728"] || problemInfo["Accepted Rate"] || void 0;
|
|
380
|
-
const testCases = [];
|
|
381
|
-
const sampleInputs = $(".sampledata").filter((_, el) => {
|
|
382
|
-
const id = $(el).attr("id");
|
|
383
|
-
return id?.startsWith("sample-input-") ?? false;
|
|
384
|
-
});
|
|
385
|
-
sampleInputs.each((_, el) => {
|
|
386
|
-
const inputId = $(el).attr("id");
|
|
387
|
-
if (!inputId) return;
|
|
388
|
-
const match = inputId.match(/sample-input-(\d+)/);
|
|
389
|
-
if (!match) return;
|
|
390
|
-
const sampleNumber = match[1];
|
|
391
|
-
const outputId = `sample-output-${sampleNumber}`;
|
|
392
|
-
const outputEl2 = $(`#${outputId}`);
|
|
393
|
-
if (outputEl2.length > 0) {
|
|
394
|
-
testCases.push({
|
|
395
|
-
input: $(el).text(),
|
|
396
|
-
output: outputEl2.text()
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
if (testCases.length === 0) {
|
|
401
|
-
$("pre").each((_, el) => {
|
|
402
|
-
const text = $(el).text().trim();
|
|
403
|
-
const prevText = $(el).prev().text().toLowerCase();
|
|
404
|
-
if (prevText.includes("\uC785\uB825") || prevText.includes("input")) {
|
|
405
|
-
const nextPre = $(el).next("pre");
|
|
406
|
-
if (nextPre.length > 0) {
|
|
407
|
-
testCases.push({
|
|
408
|
-
input: text,
|
|
409
|
-
output: nextPre.text().trim()
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
if (!title) {
|
|
416
|
-
throw new Error(
|
|
417
|
-
`\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\uAC00 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
if (!description && !inputFormat && !outputFormat) {
|
|
421
|
-
throw new Error(
|
|
422
|
-
`\uBB38\uC81C ${problemId}\uC758 \uB0B4\uC6A9\uC744 \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. BOJ \uD398\uC774\uC9C0 \uAD6C\uC870\uAC00 \uBCC0\uACBD\uB418\uC5C8\uAC70\uB098 API \uC81C\uD55C\uC5D0 \uAC78\uB838\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.`
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
title,
|
|
427
|
-
description,
|
|
428
|
-
inputFormat,
|
|
429
|
-
outputFormat,
|
|
430
|
-
testCases,
|
|
431
|
-
timeLimit,
|
|
432
|
-
memoryLimit,
|
|
433
|
-
submissions,
|
|
434
|
-
accepted,
|
|
435
|
-
acceptedUsers,
|
|
436
|
-
acceptedRate
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
201
|
// src/hooks/use-fetch-problem.ts
|
|
441
202
|
function useFetchProblem({
|
|
442
203
|
problemId,
|
package/dist/commands/open.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
} from "../chunk-
|
|
3
|
+
useOpenBrowser
|
|
4
|
+
} from "../chunk-GCOFFYJ3.js";
|
|
5
|
+
import "../chunk-QGMWUOJ3.js";
|
|
5
6
|
import {
|
|
6
7
|
Command,
|
|
7
8
|
CommandBuilder,
|
|
@@ -17,47 +18,6 @@ import {
|
|
|
17
18
|
import { StatusMessage, Alert } from "@inkjs/ui";
|
|
18
19
|
import { Spinner } from "@inkjs/ui";
|
|
19
20
|
import { Text, Box } from "ink";
|
|
20
|
-
|
|
21
|
-
// src/hooks/use-open-browser.ts
|
|
22
|
-
import { useEffect, useState } from "react";
|
|
23
|
-
var BOJ_BASE_URL = "https://www.acmicpc.net";
|
|
24
|
-
function useOpenBrowser({
|
|
25
|
-
problemId,
|
|
26
|
-
onComplete
|
|
27
|
-
}) {
|
|
28
|
-
const [status, setStatus] = useState(
|
|
29
|
-
"loading"
|
|
30
|
-
);
|
|
31
|
-
const [error, setError] = useState(null);
|
|
32
|
-
const [url, setUrl] = useState("");
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
async function handleOpenBrowser() {
|
|
35
|
-
try {
|
|
36
|
-
const problemUrl = `${BOJ_BASE_URL}/problem/${problemId}`;
|
|
37
|
-
setUrl(problemUrl);
|
|
38
|
-
await openBrowser(problemUrl);
|
|
39
|
-
setStatus("success");
|
|
40
|
-
setTimeout(() => {
|
|
41
|
-
onComplete?.();
|
|
42
|
-
}, 1500);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
setStatus("error");
|
|
45
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
onComplete?.();
|
|
48
|
-
}, 2e3);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
void handleOpenBrowser();
|
|
52
|
-
}, [problemId, onComplete]);
|
|
53
|
-
return {
|
|
54
|
-
status,
|
|
55
|
-
error,
|
|
56
|
-
url
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// src/commands/open.tsx
|
|
61
21
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
62
22
|
function OpenView({ problemId, onComplete }) {
|
|
63
23
|
const { status, error, url } = useOpenBrowser({
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
searchProblems
|
|
4
|
+
} from "../chunk-AG6KWWHS.js";
|
|
5
|
+
import {
|
|
6
|
+
useOpenBrowser
|
|
7
|
+
} from "../chunk-GCOFFYJ3.js";
|
|
8
|
+
import "../chunk-QGMWUOJ3.js";
|
|
9
|
+
import {
|
|
10
|
+
getTierColor,
|
|
11
|
+
getTierName,
|
|
12
|
+
source_default
|
|
13
|
+
} from "../chunk-HDNNR5OY.js";
|
|
14
|
+
import {
|
|
15
|
+
Command,
|
|
16
|
+
CommandBuilder,
|
|
17
|
+
CommandDef,
|
|
18
|
+
getProblemDirPath
|
|
19
|
+
} from "../chunk-7SVCS23X.js";
|
|
20
|
+
import {
|
|
21
|
+
__decorateClass
|
|
22
|
+
} from "../chunk-7MQMPJ3X.js";
|
|
23
|
+
|
|
24
|
+
// src/commands/search.tsx
|
|
25
|
+
import { existsSync } from "fs";
|
|
26
|
+
import { Alert, Select, Spinner } from "@inkjs/ui";
|
|
27
|
+
import { Box, Text } from "ink";
|
|
28
|
+
import { useEffect, useState } from "react";
|
|
29
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
30
|
+
function OpenBrowserView({ problemId, onComplete }) {
|
|
31
|
+
const { status, error, url } = useOpenBrowser({
|
|
32
|
+
problemId,
|
|
33
|
+
onComplete
|
|
34
|
+
});
|
|
35
|
+
if (status === "loading") {
|
|
36
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
37
|
+
/* @__PURE__ */ jsx(Spinner, { label: "\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5EC\uB294 \uC911..." }),
|
|
38
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
39
|
+
"\uBB38\uC81C #",
|
|
40
|
+
problemId
|
|
41
|
+
] }) })
|
|
42
|
+
] });
|
|
43
|
+
}
|
|
44
|
+
if (status === "error") {
|
|
45
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
46
|
+
/* @__PURE__ */ jsxs(Alert, { variant: "error", children: [
|
|
47
|
+
"\uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ",
|
|
48
|
+
error
|
|
49
|
+
] }),
|
|
50
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
51
|
+
"URL: ",
|
|
52
|
+
url
|
|
53
|
+
] }) })
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
57
|
+
/* @__PURE__ */ jsx(Alert, { variant: "success", children: "\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uBB38\uC81C \uD398\uC774\uC9C0\uB97C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4!" }),
|
|
58
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
59
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
60
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\uBB38\uC81C \uBC88\uD638:" }),
|
|
61
|
+
" ",
|
|
62
|
+
problemId
|
|
63
|
+
] }),
|
|
64
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
65
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "URL:" }),
|
|
66
|
+
" ",
|
|
67
|
+
/* @__PURE__ */ jsx(Text, { color: "blue", underline: true, children: url })
|
|
68
|
+
] })
|
|
69
|
+
] })
|
|
70
|
+
] });
|
|
71
|
+
}
|
|
72
|
+
function SearchView({ query, onComplete }) {
|
|
73
|
+
const [results, setResults] = useState([]);
|
|
74
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
75
|
+
const [totalPages, setTotalPages] = useState(1);
|
|
76
|
+
const [loading, setLoading] = useState(true);
|
|
77
|
+
const [error, setError] = useState(null);
|
|
78
|
+
const [selectedProblemId, setSelectedProblemId] = useState(
|
|
79
|
+
null
|
|
80
|
+
);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
async function performSearch() {
|
|
83
|
+
try {
|
|
84
|
+
setLoading(true);
|
|
85
|
+
setError(null);
|
|
86
|
+
const searchResults = await searchProblems(query, currentPage);
|
|
87
|
+
const resultsWithSolvedStatus = searchResults.problems.map(
|
|
88
|
+
(problem) => {
|
|
89
|
+
const problemDirPath = getProblemDirPath(problem.problemId);
|
|
90
|
+
const isSolved = existsSync(problemDirPath);
|
|
91
|
+
return {
|
|
92
|
+
...problem,
|
|
93
|
+
isSolved
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
setResults(resultsWithSolvedStatus);
|
|
98
|
+
setTotalPages(searchResults.totalPages);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
101
|
+
} finally {
|
|
102
|
+
setLoading(false);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
void performSearch();
|
|
106
|
+
}, [query, currentPage]);
|
|
107
|
+
if (loading && !selectedProblemId) {
|
|
108
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
109
|
+
/* @__PURE__ */ jsx(Spinner, { label: "\uAC80\uC0C9 \uC911..." }),
|
|
110
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
111
|
+
"\uCFFC\uB9AC: ",
|
|
112
|
+
query
|
|
113
|
+
] }) })
|
|
114
|
+
] });
|
|
115
|
+
}
|
|
116
|
+
if (error && !selectedProblemId) {
|
|
117
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
118
|
+
/* @__PURE__ */ jsxs(Alert, { variant: "error", children: [
|
|
119
|
+
"\uAC80\uC0C9 \uC2E4\uD328: ",
|
|
120
|
+
error
|
|
121
|
+
] }),
|
|
122
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
123
|
+
"\uCFFC\uB9AC: ",
|
|
124
|
+
query
|
|
125
|
+
] }) })
|
|
126
|
+
] });
|
|
127
|
+
}
|
|
128
|
+
if (selectedProblemId) {
|
|
129
|
+
return /* @__PURE__ */ jsx(OpenBrowserView, { problemId: selectedProblemId, onComplete });
|
|
130
|
+
}
|
|
131
|
+
if (results.length === 0) {
|
|
132
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
133
|
+
/* @__PURE__ */ jsx(Alert, { variant: "info", children: "\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }),
|
|
134
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
135
|
+
"\uCFFC\uB9AC: ",
|
|
136
|
+
query
|
|
137
|
+
] }) })
|
|
138
|
+
] });
|
|
139
|
+
}
|
|
140
|
+
const options = [];
|
|
141
|
+
results.forEach((problem) => {
|
|
142
|
+
const solvedText = problem.solvedCount ? ` (${problem.solvedCount.toLocaleString()}\uBA85` : "";
|
|
143
|
+
const triesText = problem.averageTries ? `, \uD3C9\uADE0 ${problem.averageTries}\uD68C` : "";
|
|
144
|
+
const suffix = solvedText + triesText + (solvedText ? ")" : "");
|
|
145
|
+
const solvedMark = problem.isSolved ? " \u2713" : "";
|
|
146
|
+
let tierText = "";
|
|
147
|
+
if (problem.level) {
|
|
148
|
+
const tierName = getTierName(problem.level);
|
|
149
|
+
const tierColor = getTierColor(problem.level);
|
|
150
|
+
if (typeof tierColor === "string") {
|
|
151
|
+
tierText = ` ${source_default.bold.hex(tierColor)(tierName)}`;
|
|
152
|
+
} else {
|
|
153
|
+
tierText = ` ${tierColor(source_default.bold(tierName))}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
options.push({
|
|
157
|
+
label: `${tierText} ${problem.problemId} - ${problem.title}${solvedMark}${suffix}`,
|
|
158
|
+
value: `problem:${problem.problemId}`
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
if (currentPage < totalPages) {
|
|
162
|
+
options.push({
|
|
163
|
+
label: `\u2192 \uB2E4\uC74C \uD398\uC774\uC9C0 (${currentPage + 1}/${totalPages})`,
|
|
164
|
+
value: "next-page"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (currentPage > 1) {
|
|
168
|
+
options.push({
|
|
169
|
+
label: `\u2190 \uC774\uC804 \uD398\uC774\uC9C0 (${currentPage - 1}/${totalPages})`,
|
|
170
|
+
value: "prev-page"
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const handleSelect = (value) => {
|
|
174
|
+
if (value === "next-page") {
|
|
175
|
+
setCurrentPage(currentPage + 1);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (value === "prev-page") {
|
|
179
|
+
setCurrentPage(currentPage - 1);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (value.startsWith("problem:")) {
|
|
183
|
+
const problemId = parseInt(value.replace("problem:", ""), 10);
|
|
184
|
+
if (!isNaN(problemId)) {
|
|
185
|
+
setSelectedProblemId(problemId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
190
|
+
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "\u{1F50D} \uAC80\uC0C9 \uACB0\uACFC" }) }),
|
|
191
|
+
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
192
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
193
|
+
"\uCFFC\uB9AC: ",
|
|
194
|
+
query
|
|
195
|
+
] }),
|
|
196
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
197
|
+
" ",
|
|
198
|
+
"(\uD398\uC774\uC9C0 ",
|
|
199
|
+
currentPage,
|
|
200
|
+
"/",
|
|
201
|
+
totalPages,
|
|
202
|
+
")"
|
|
203
|
+
] })
|
|
204
|
+
] }),
|
|
205
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Select, { options, onChange: handleSelect }) })
|
|
206
|
+
] });
|
|
207
|
+
}
|
|
208
|
+
var SearchCommand = class extends Command {
|
|
209
|
+
async execute(args, _flags) {
|
|
210
|
+
const query = args.join(" ").trim();
|
|
211
|
+
if (!query) {
|
|
212
|
+
console.error("\uC624\uB958: \uAC80\uC0C9 \uCFFC\uB9AC\uB97C \uC785\uB825\uD574\uC8FC\uC138\uC694.");
|
|
213
|
+
console.error(`\uC0AC\uC6A9\uBC95: ps search <\uCFFC\uB9AC>`);
|
|
214
|
+
console.error(`\uB3C4\uC6C0\uB9D0: ps search --help`);
|
|
215
|
+
console.error(`\uC608\uC81C: ps search "*g1...g5"`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await this.renderView(SearchView, {
|
|
220
|
+
query
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
SearchCommand = __decorateClass([
|
|
225
|
+
CommandDef({
|
|
226
|
+
name: "search",
|
|
227
|
+
description: `solved.ac\uC5D0\uC11C \uBB38\uC81C\uB97C \uAC80\uC0C9\uD558\uACE0 \uC120\uD0DD\uD55C \uBB38\uC81C\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5FD\uB2C8\uB2E4.
|
|
228
|
+
- solved.ac \uAC80\uC0C9\uC5B4 \uBB38\uBC95\uC744 \uC9C0\uC6D0\uD569\uB2C8\uB2E4.
|
|
229
|
+
- \uBB38\uC81C \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD\uD558\uBA74 \uC790\uB3D9\uC73C\uB85C \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uBB38\uC81C \uD398\uC774\uC9C0\uB97C \uC5FD\uB2C8\uB2E4.
|
|
230
|
+
- \uD398\uC774\uC9C0\uB124\uC774\uC158\uC744 \uD1B5\uD574 \uC5EC\uB7EC \uD398\uC774\uC9C0\uC758 \uACB0\uACFC\uB97C \uD0D0\uC0C9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`,
|
|
231
|
+
autoDetectProblemId: false,
|
|
232
|
+
requireProblemId: false,
|
|
233
|
+
examples: [
|
|
234
|
+
'search "*g1...g5" # Gold 1-5 \uBB38\uC81C \uAC80\uC0C9',
|
|
235
|
+
'search "tier:g1...g5" # Gold 1-5 \uBB38\uC81C \uAC80\uC0C9 (tier: \uBB38\uBC95)',
|
|
236
|
+
'search "#dp" # DP \uD0DC\uADF8 \uBB38\uC81C \uAC80\uC0C9',
|
|
237
|
+
'search "tag:dp" # DP \uD0DC\uADF8 \uBB38\uC81C \uAC80\uC0C9 (tag: \uBB38\uBC95)',
|
|
238
|
+
'search "*g1...g5 #dp" # Gold 1-5 \uD2F0\uC5B4\uC758 DP \uD0DC\uADF8 \uBB38\uC81C \uAC80\uC0C9'
|
|
239
|
+
]
|
|
240
|
+
})
|
|
241
|
+
], SearchCommand);
|
|
242
|
+
var search_default = CommandBuilder.fromClass(SearchCommand);
|
|
243
|
+
export {
|
|
244
|
+
SearchCommand,
|
|
245
|
+
search_default as default
|
|
246
|
+
};
|
package/dist/commands/stats.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
getUserStats
|
|
4
|
+
} from "../chunk-A6STXEAE.js";
|
|
5
|
+
import {
|
|
6
|
+
calculateTierProgress,
|
|
7
|
+
getNextTierMinRating,
|
|
3
8
|
getTierColor,
|
|
4
9
|
getTierName,
|
|
5
|
-
getUserStats,
|
|
6
10
|
source_default
|
|
7
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-HDNNR5OY.js";
|
|
8
12
|
import {
|
|
9
13
|
Command,
|
|
10
14
|
CommandBuilder,
|
|
@@ -18,8 +22,7 @@ import {
|
|
|
18
22
|
// src/commands/stats.tsx
|
|
19
23
|
import { Alert } from "@inkjs/ui";
|
|
20
24
|
import { Spinner } from "@inkjs/ui";
|
|
21
|
-
import
|
|
22
|
-
import { Box, Text } from "ink";
|
|
25
|
+
import { Box, Text, Transform } from "ink";
|
|
23
26
|
|
|
24
27
|
// src/hooks/use-user-stats.ts
|
|
25
28
|
import { useEffect, useState } from "react";
|
|
@@ -56,6 +59,16 @@ function useUserStats({
|
|
|
56
59
|
|
|
57
60
|
// src/commands/stats.tsx
|
|
58
61
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
62
|
+
function ProgressBarWithColor({ value, colorFn }) {
|
|
63
|
+
const width = process.stdout.columns || 40;
|
|
64
|
+
const barWidth = Math.max(10, Math.min(30, width - 20));
|
|
65
|
+
const filled = Math.round(value / 100 * barWidth);
|
|
66
|
+
const empty = barWidth - filled;
|
|
67
|
+
const filledBar = "\u2588".repeat(filled);
|
|
68
|
+
const emptyBar = "\u2591".repeat(empty);
|
|
69
|
+
const barText = filledBar + emptyBar;
|
|
70
|
+
return /* @__PURE__ */ jsx(Transform, { transform: (output) => colorFn(output), children: /* @__PURE__ */ jsx(Text, { children: barText }) });
|
|
71
|
+
}
|
|
59
72
|
function StatsView({ handle, onComplete }) {
|
|
60
73
|
const { status, user, error } = useUserStats({
|
|
61
74
|
handle,
|
|
@@ -72,16 +85,22 @@ function StatsView({ handle, onComplete }) {
|
|
|
72
85
|
}
|
|
73
86
|
if (user) {
|
|
74
87
|
const tierName = getTierName(user.tier);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
])(tierName) : source_default.hex(getTierColor(user.tier))(tierName);
|
|
88
|
+
const tierColor = getTierColor(user.tier);
|
|
89
|
+
const tierColorFn = typeof tierColor === "string" ? source_default.hex(tierColor) : tierColor.multiline;
|
|
90
|
+
const nextTierMin = getNextTierMinRating(user.tier);
|
|
91
|
+
const progress = user.tier === 31 ? 100 : calculateTierProgress(user.rating, user.tier);
|
|
80
92
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
81
93
|
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
|
|
82
94
|
"\u2728 ",
|
|
83
95
|
user.handle
|
|
84
96
|
] }) }),
|
|
97
|
+
/* @__PURE__ */ jsx(Box, { marginBottom: 1, flexDirection: "row", gap: 1, children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
98
|
+
tierColorFn(tierName),
|
|
99
|
+
" ",
|
|
100
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: tierColorFn(user.rating.toLocaleString()) }),
|
|
101
|
+
nextTierMin !== null && /* @__PURE__ */ jsx(Text, { bold: true, children: " / " + nextTierMin.toLocaleString() })
|
|
102
|
+
] }) }),
|
|
103
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx(ProgressBarWithColor, { value: progress, colorFn: tierColorFn }) }),
|
|
85
104
|
/* @__PURE__ */ jsx(
|
|
86
105
|
Box,
|
|
87
106
|
{
|
|
@@ -90,11 +109,6 @@ function StatsView({ handle, onComplete }) {
|
|
|
90
109
|
borderColor: "gray",
|
|
91
110
|
alignSelf: "flex-start",
|
|
92
111
|
children: /* @__PURE__ */ jsxs(Box, { paddingX: 1, paddingY: 0, flexDirection: "column", children: [
|
|
93
|
-
/* @__PURE__ */ jsxs(Text, { children: [
|
|
94
|
-
tierDisplay,
|
|
95
|
-
" ",
|
|
96
|
-
/* @__PURE__ */ jsx(Text, { bold: true, children: user.rating.toLocaleString() })
|
|
97
|
-
] }),
|
|
98
112
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
99
113
|
"\uD574\uACB0\uD55C \uBB38\uC81C:",
|
|
100
114
|
" ",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhseung/ps-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "백준(BOJ) 문제 해결을 위한 통합 CLI 도구",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dev": "tsup --watch",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"lint": "eslint",
|
|
18
|
-
"format": "prettier",
|
|
18
|
+
"format": "prettier --check .",
|
|
19
19
|
"check": "prettier --write . && eslint --fix",
|
|
20
20
|
"patch": "npm version patch && bun publish --access=public",
|
|
21
21
|
"minor": "npm version minor && bun publish --access=public",
|