@rhseung/ps-cli 1.4.0 → 1.6.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,255 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/tier.ts
4
+ import gradient from "gradient-string";
5
+ var TIER_NAMES = [
6
+ void 0,
7
+ "Bronze V",
8
+ "Bronze IV",
9
+ "Bronze III",
10
+ "Bronze II",
11
+ "Bronze I",
12
+ "Silver V",
13
+ "Silver IV",
14
+ "Silver III",
15
+ "Silver II",
16
+ "Silver I",
17
+ "Gold V",
18
+ "Gold IV",
19
+ "Gold III",
20
+ "Gold II",
21
+ "Gold I",
22
+ "Platinum V",
23
+ "Platinum IV",
24
+ "Platinum III",
25
+ "Platinum II",
26
+ "Platinum I",
27
+ "Diamond V",
28
+ "Diamond IV",
29
+ "Diamond III",
30
+ "Diamond II",
31
+ "Diamond I",
32
+ "Ruby V",
33
+ "Ruby IV",
34
+ "Ruby III",
35
+ "Ruby II",
36
+ "Ruby I",
37
+ "Master"
38
+ ];
39
+ var TIER_COLORS = [
40
+ void 0,
41
+ "#9d4900",
42
+ "#a54f00",
43
+ "#ad5600",
44
+ "#b55d0a",
45
+ "#c67739",
46
+ "#38546e",
47
+ "#3d5a74",
48
+ "#435f7a",
49
+ "#496580",
50
+ "#4e6a86",
51
+ "#d28500",
52
+ "#df8f00",
53
+ "#ec9a00",
54
+ "#f9a518",
55
+ "#ffb028",
56
+ "#00c78b",
57
+ "#00d497",
58
+ "#27e2a4",
59
+ "#3ef0b1",
60
+ "#51fdbd",
61
+ "#009ee5",
62
+ "#00a9f0",
63
+ "#00b4fc",
64
+ "#2bbfff",
65
+ "#41caff",
66
+ "#e0004c",
67
+ "#ea0053",
68
+ "#f5005a",
69
+ "#ff0062",
70
+ "#ff3071",
71
+ "#b300e0"
72
+ ];
73
+ var TIER_IMAGE_BASE_URL = "https://d2gd6pc034wcta.cloudfront.net/tier";
74
+ var MASTER_TIER_GRADIENT = [
75
+ { r: 255, g: 124, b: 168 },
76
+ { r: 180, g: 145, b: 255 },
77
+ { r: 124, g: 249, b: 255 }
78
+ ];
79
+ var TIER_MIN_RATINGS = [
80
+ 0,
81
+ // Unrated (tier 0): 0-29
82
+ 30,
83
+ // Bronze V (tier 1)
84
+ 60,
85
+ // Bronze IV (tier 2)
86
+ 90,
87
+ // Bronze III (tier 3)
88
+ 120,
89
+ // Bronze II (tier 4)
90
+ 150,
91
+ // Bronze I (tier 5)
92
+ 200,
93
+ // Silver V (tier 6)
94
+ 300,
95
+ // Silver IV (tier 7)
96
+ 400,
97
+ // Silver III (tier 8)
98
+ 500,
99
+ // Silver II (tier 9)
100
+ 650,
101
+ // Silver I (tier 10)
102
+ 800,
103
+ // Gold V (tier 11)
104
+ 950,
105
+ // Gold IV (tier 12)
106
+ 1100,
107
+ // Gold III (tier 13)
108
+ 1250,
109
+ // Gold II (tier 14)
110
+ 1400,
111
+ // Gold I (tier 15)
112
+ 1600,
113
+ // Platinum V (tier 16)
114
+ 1750,
115
+ // Platinum IV (tier 17)
116
+ 1900,
117
+ // Platinum III (tier 18)
118
+ 2e3,
119
+ // Platinum II (tier 19)
120
+ 2100,
121
+ // Platinum I (tier 20)
122
+ 2200,
123
+ // Diamond V (tier 21)
124
+ 2300,
125
+ // Diamond IV (tier 22)
126
+ 2400,
127
+ // Diamond III (tier 23)
128
+ 2500,
129
+ // Diamond II (tier 24)
130
+ 2600,
131
+ // Diamond I (tier 25)
132
+ 2700,
133
+ // Ruby V (tier 26)
134
+ 2800,
135
+ // Ruby IV (tier 27)
136
+ 2850,
137
+ // Ruby III (tier 28)
138
+ 2900,
139
+ // Ruby II (tier 29)
140
+ 2950,
141
+ // Ruby I (tier 30)
142
+ 3e3
143
+ // Master (tier 31)
144
+ ];
145
+ function getTierMinRating(tier) {
146
+ if (tier >= 0 && tier < TIER_MIN_RATINGS.length) {
147
+ return TIER_MIN_RATINGS[tier] ?? 0;
148
+ }
149
+ return 0;
150
+ }
151
+ function getNextTierMinRating(tier) {
152
+ if (tier === 31) {
153
+ return null;
154
+ }
155
+ if (tier >= 0 && tier < TIER_MIN_RATINGS.length - 1) {
156
+ return TIER_MIN_RATINGS[tier + 1] ?? null;
157
+ }
158
+ return null;
159
+ }
160
+ function calculateTierProgress(currentRating, tier) {
161
+ if (tier === 31) {
162
+ return 100;
163
+ }
164
+ const currentTierMin = getTierMinRating(tier);
165
+ const nextTierMin = getNextTierMinRating(tier);
166
+ if (nextTierMin === null) {
167
+ return 100;
168
+ }
169
+ if (currentRating < currentTierMin) {
170
+ return 0;
171
+ }
172
+ if (currentRating >= nextTierMin) {
173
+ return 100;
174
+ }
175
+ const progress = (currentRating - currentTierMin) / (nextTierMin - currentTierMin) * 100;
176
+ return Math.max(0, Math.min(100, progress));
177
+ }
178
+ function getTierName(level) {
179
+ if (level === 0) return "Unrated";
180
+ if (level >= 1 && level < TIER_NAMES.length) {
181
+ return TIER_NAMES[level] || "Unrated";
182
+ }
183
+ return "Unrated";
184
+ }
185
+ function getTierColor(level) {
186
+ if (level === 0) return "#2d2d2d";
187
+ if (level === 31) {
188
+ return gradient([...MASTER_TIER_GRADIENT]);
189
+ }
190
+ if (level >= 1 && level < TIER_COLORS.length) {
191
+ return TIER_COLORS[level] || "#2d2d2d";
192
+ }
193
+ return "#2d2d2d";
194
+ }
195
+ function getTierImageUrl(level) {
196
+ return `${TIER_IMAGE_BASE_URL}/${level}.svg`;
197
+ }
198
+
199
+ // src/services/solved-api.ts
200
+ var BASE_URL = "https://solved.ac/api/v3";
201
+ var USER_AGENT = "ps-cli/1.0.0";
202
+ async function fetchWithRetry(url, options = {}, maxRetries = 3) {
203
+ let lastError = null;
204
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
205
+ try {
206
+ const response = await fetch(url, {
207
+ ...options,
208
+ headers: {
209
+ "User-Agent": USER_AGENT,
210
+ ...options.headers
211
+ }
212
+ });
213
+ if (response.status === 429) {
214
+ const retryAfter = response.headers.get("Retry-After");
215
+ const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1e3 : (attempt + 1) * 1e3;
216
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
217
+ continue;
218
+ }
219
+ if (!response.ok) {
220
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
221
+ }
222
+ return response;
223
+ } catch (error) {
224
+ lastError = error instanceof Error ? error : new Error(String(error));
225
+ if (attempt < maxRetries - 1) {
226
+ await new Promise(
227
+ (resolve) => setTimeout(resolve, (attempt + 1) * 1e3)
228
+ );
229
+ }
230
+ }
231
+ }
232
+ throw lastError || new Error("Request failed after retries");
233
+ }
234
+ async function getProblem(problemId) {
235
+ const url = `${BASE_URL}/problem/show?problemId=${problemId}`;
236
+ const response = await fetchWithRetry(url);
237
+ const data = await response.json();
238
+ return data;
239
+ }
240
+ async function getUserStats(handle) {
241
+ const url = `${BASE_URL}/user/show?handle=${handle}`;
242
+ const response = await fetchWithRetry(url);
243
+ const data = await response.json();
244
+ return data;
245
+ }
246
+
247
+ export {
248
+ getNextTierMinRating,
249
+ calculateTierProgress,
250
+ getTierName,
251
+ getTierColor,
252
+ getTierImageUrl,
253
+ getProblem,
254
+ getUserStats
255
+ };
@@ -9924,8 +9924,10 @@ var config = new Conf({
9924
9924
  autoOpenEditor: false,
9925
9925
  // 기본값: 자동 열기 비활성화
9926
9926
  solvedAcHandle: void 0,
9927
- problemDir: "problems"
9927
+ problemDir: "problems",
9928
9928
  // 기본값: problems 디렉토리
9929
+ solvingDir: "solving"
9930
+ // 기본값: solving 디렉토리
9929
9931
  }
9930
9932
  });
9931
9933
  var projectConfigCache = null;
@@ -10014,44 +10016,58 @@ function getProblemDir() {
10014
10016
  }
10015
10017
  return config.get("problemDir") ?? "problems";
10016
10018
  }
10019
+ function getSolvingDir() {
10020
+ const projectConfig = getProjectConfigSync();
10021
+ if (projectConfig?.solvingDir !== void 0) {
10022
+ return projectConfig.solvingDir;
10023
+ }
10024
+ return config.get("solvingDir") ?? "solving";
10025
+ }
10017
10026
 
10018
10027
  // src/utils/problem-id.ts
10019
10028
  import { join as join2 } from "path";
10020
10029
  function detectProblemIdFromPath(cwd = process.cwd()) {
10021
10030
  const problemDir = getProblemDir();
10031
+ const solvingDir = getSolvingDir();
10022
10032
  const normalizedPath = cwd.replace(/\\/g, "/");
10023
- if (problemDir === "." || problemDir === "") {
10033
+ const dirsToCheck = [problemDir, solvingDir].filter(
10034
+ (dir) => dir && dir !== "." && dir !== ""
10035
+ );
10036
+ if (dirsToCheck.length === 0) {
10024
10037
  const segments = normalizedPath.split("/").filter(Boolean);
10025
10038
  const lastSegment = segments[segments.length - 1];
10026
10039
  if (lastSegment) {
10027
- const problemId2 = parseInt(lastSegment, 10);
10028
- if (!isNaN(problemId2) && problemId2 > 0 && lastSegment === problemId2.toString()) {
10029
- return problemId2;
10040
+ const problemId = parseInt(lastSegment, 10);
10041
+ if (!isNaN(problemId) && problemId > 0 && lastSegment === problemId.toString()) {
10042
+ return problemId;
10030
10043
  }
10031
10044
  }
10032
10045
  return null;
10033
10046
  }
10034
- const dirPattern = `/${problemDir}/`;
10035
- const dirIndex = normalizedPath.indexOf(dirPattern);
10036
- if (dirIndex === -1) {
10037
- return null;
10038
- }
10039
- const afterDir = normalizedPath.substring(dirIndex + dirPattern.length);
10040
- if (!afterDir) {
10041
- return null;
10042
- }
10043
- const firstSegment = afterDir.split("/")[0];
10044
- if (!firstSegment) {
10045
- return null;
10046
- }
10047
- const problemId = parseInt(firstSegment, 10);
10048
- if (isNaN(problemId) || problemId <= 0) {
10049
- return null;
10050
- }
10051
- if (firstSegment !== problemId.toString()) {
10052
- return null;
10047
+ for (const dir of dirsToCheck) {
10048
+ const dirPattern = `/${dir}/`;
10049
+ const dirIndex = normalizedPath.indexOf(dirPattern);
10050
+ if (dirIndex === -1) {
10051
+ continue;
10052
+ }
10053
+ const afterDir = normalizedPath.substring(dirIndex + dirPattern.length);
10054
+ if (!afterDir) {
10055
+ continue;
10056
+ }
10057
+ const firstSegment = afterDir.split("/")[0];
10058
+ if (!firstSegment) {
10059
+ continue;
10060
+ }
10061
+ const problemId = parseInt(firstSegment, 10);
10062
+ if (isNaN(problemId) || problemId <= 0) {
10063
+ continue;
10064
+ }
10065
+ if (firstSegment !== problemId.toString()) {
10066
+ continue;
10067
+ }
10068
+ return problemId;
10053
10069
  }
10054
- return problemId;
10070
+ return null;
10055
10071
  }
10056
10072
  function getProblemId(args, cwd = process.cwd()) {
10057
10073
  if (args.length > 0 && args[0]) {
@@ -10071,6 +10087,15 @@ function getProblemDirPath(problemId, cwd = process.cwd()) {
10071
10087
  }
10072
10088
  return join2(baseDir, problemDir, problemId.toString());
10073
10089
  }
10090
+ function getSolvingDirPath(problemId, cwd = process.cwd()) {
10091
+ const solvingDir = getSolvingDir();
10092
+ const projectRoot = findProjectRoot(cwd);
10093
+ const baseDir = projectRoot || cwd;
10094
+ if (solvingDir === "." || solvingDir === "") {
10095
+ return join2(baseDir, problemId.toString());
10096
+ }
10097
+ return join2(baseDir, solvingDir, problemId.toString());
10098
+ }
10074
10099
 
10075
10100
  // src/utils/execution-context.ts
10076
10101
  import { readdir } from "fs/promises";
@@ -10249,14 +10274,17 @@ var CommandBuilder = class {
10249
10274
 
10250
10275
  export {
10251
10276
  Command,
10277
+ findProjectRoot,
10252
10278
  getDefaultLanguage,
10253
10279
  getEditor,
10254
10280
  getAutoOpenEditor,
10255
10281
  getSolvedAcHandle,
10256
10282
  getProblemDir,
10283
+ getSolvingDir,
10257
10284
  detectProblemIdFromPath,
10258
10285
  getProblemId,
10259
10286
  getProblemDirPath,
10287
+ getSolvingDirPath,
10260
10288
  resolveProblemContext,
10261
10289
  resolveLanguage,
10262
10290
  findSolutionFile,
@@ -8,7 +8,7 @@ import {
8
8
  getEditor,
9
9
  getProblemDir,
10
10
  getSolvedAcHandle
11
- } from "../chunk-7SVCS23X.js";
11
+ } from "../chunk-RVD22OUQ.js";
12
12
  import {
13
13
  __decorateClass,
14
14
  getSupportedLanguages,
@@ -1,20 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ scrapeProblem
4
+ } from "../chunk-AG6KWWHS.js";
2
5
  import {
3
6
  getProblem,
4
7
  getTierColor,
5
8
  getTierImageUrl,
6
- getTierName,
7
- source_default
8
- } from "../chunk-NB4OIWND.js";
9
+ getTierName
10
+ } from "../chunk-NH36IFWR.js";
9
11
  import {
10
12
  Command,
11
13
  CommandBuilder,
12
14
  CommandDef,
13
15
  getAutoOpenEditor,
14
16
  getEditor,
15
- getProblemDirPath,
16
- getProblemId
17
- } from "../chunk-7SVCS23X.js";
17
+ getProblemId,
18
+ getSolvingDirPath
19
+ } from "../chunk-RVD22OUQ.js";
18
20
  import {
19
21
  __decorateClass,
20
22
  getLanguageConfig,
@@ -33,16 +35,18 @@ import { jsx, jsxs } from "react/jsx-runtime";
33
35
  function ProblemDashboard({ problem }) {
34
36
  const tierName = getTierName(problem.level);
35
37
  const tierColor = getTierColor(problem.level);
38
+ const borderColorString = typeof tierColor === "string" ? tierColor : "#ff7ca8";
39
+ const textColorString = borderColorString;
36
40
  return /* @__PURE__ */ jsx(
37
41
  Box,
38
42
  {
39
43
  flexDirection: "column",
40
44
  borderStyle: "round",
41
- borderColor: tierColor,
45
+ borderColor: borderColorString,
42
46
  paddingX: 1,
43
47
  alignSelf: "flex-start",
44
- children: /* @__PURE__ */ jsxs(Text, { bold: true, children: [
45
- source_default.hex(tierColor)(tierName),
48
+ children: /* @__PURE__ */ jsxs(Text, { bold: true, color: textColorString, children: [
49
+ tierName,
46
50
  " ",
47
51
  /* @__PURE__ */ jsxs(Text, { color: "white", children: [
48
52
  "#",
@@ -80,7 +84,7 @@ function getProjectRoot() {
80
84
  return join(__dirname, "../..");
81
85
  }
82
86
  async function generateProblemFiles(problem, language = "python") {
83
- const problemDir = getProblemDirPath(problem.id);
87
+ const problemDir = getSolvingDirPath(problem.id);
84
88
  await mkdir(problemDir, { recursive: true });
85
89
  const langConfig = getLanguageConfig(language);
86
90
  const projectRoot = getProjectRoot();
@@ -191,252 +195,6 @@ ${tags}
191
195
  return problemDir;
192
196
  }
193
197
 
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
198
  // src/hooks/use-fetch-problem.ts
441
199
  function useFetchProblem({
442
200
  problemId,