@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.
@@ -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 += `![${imgAlt}](${imageUrl})`;
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
  };
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- getProblem,
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-NB4OIWND.js";
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
- return /* @__PURE__ */ jsx(
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: tierColor,
48
+ borderColor: borderColorString,
42
49
  paddingX: 1,
43
50
  alignSelf: "flex-start",
44
51
  children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
45
- source_default.hex(tierColor)(tierName),
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 += `![${imgAlt}](${imageUrl})`;
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,
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- openBrowser
4
- } from "../chunk-QGMWUOJ3.js";
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
+ };
@@ -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-NB4OIWND.js";
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 gradient from "gradient-string";
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 tierDisplay = user.tier === 31 ? gradient([
76
- { r: 255, g: 124, b: 168 },
77
- { r: 180, g: 145, b: 255 },
78
- { r: 124, g: 249, b: 255 }
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.4.0",
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",