From 87bf0b91ee6844deb6dd19476cdc96152e403e96 Mon Sep 17 00:00:00 2001 From: sounmind Date: Wed, 22 Apr 2026 08:12:29 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20reduce=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=92=80=EC=9D=B4=20=ED=98=84=ED=99=A9=20to=20Blind=20Top=2075?= =?UTF-8?q?'s=2010=20categories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LeetCode 기준 20+개 카테고리가 나열되며 표가 길어지던 문제를 Blind Top 75 큐레이션의 10개 카테고리로 축소한다. - utils/blindCategories.js: 75개 문제별 Blind 카테고리 매핑 테이블 - handlers/learning-status.js: buildCategoryProgress 가 Blind 10개만 집계 Closes #34 Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: soobing <16860535+soobing@users.noreply.github.com> --- handlers/learning-status.js | 27 +++++-- utils/blindCategories.js | 136 ++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 utils/blindCategories.js diff --git a/handlers/learning-status.js b/handlers/learning-status.js index 4cb9bf6..91772ab 100644 --- a/handlers/learning-status.js +++ b/handlers/learning-status.js @@ -15,11 +15,18 @@ import { formatLearningStatusComment, upsertLearningStatusComment, } from "../utils/learningComment.js"; +import { + BLIND_CATEGORY_ORDER, + getBlindCategories, +} from "../utils/blindCategories.js"; const MAX_FILE_SIZE = 15000; // 15K 문자 제한 (OpenAI 토큰 안전장치) /** - * 카테고리별로 누적 풀이 진행도를 계산한다. + * Blind Top 75 카테고리별로 누적 풀이 진행도를 계산한다. + * + * `problem-categories.json` 의 LeetCode 세부 카테고리(20+개) 대신 + * Blind 10 카테고리만 표로 노출한다 (이슈 #34). * * @param {object} categories - problem-categories.json 전체 오브젝트 * @param {string[]} solvedProblems - 사용자가 풀이한 문제 이름 배열 @@ -27,14 +34,18 @@ const MAX_FILE_SIZE = 15000; // 15K 문자 제한 (OpenAI 토큰 안전장치) */ function buildCategoryProgress(categories, solvedProblems) { const solvedSet = new Set(solvedProblems); - const categoryMap = new Map(); + const categoryMap = new Map( + BLIND_CATEGORY_ORDER.map((cat) => [ + cat, + { total: 0, solved: 0, solvedDifficulties: [] }, + ]) + ); for (const [problemName, info] of Object.entries(categories)) { - for (const cat of info.categories) { - if (!categoryMap.has(cat)) { - categoryMap.set(cat, { total: 0, solved: 0, solvedDifficulties: [] }); - } + const blindCategories = getBlindCategories(problemName); + for (const cat of blindCategories) { const entry = categoryMap.get(cat); + if (!entry) continue; entry.total++; if (solvedSet.has(problemName)) { entry.solved++; @@ -48,7 +59,9 @@ function buildCategoryProgress(categories, solvedProblems) { const ratioA = a[1].total > 0 ? a[1].solved / a[1].total : 0; const ratioB = b[1].total > 0 ? b[1].solved / b[1].total : 0; if (ratioB !== ratioA) return ratioB - ratioA; - return a[0].localeCompare(b[0]); + return ( + BLIND_CATEGORY_ORDER.indexOf(a[0]) - BLIND_CATEGORY_ORDER.indexOf(b[0]) + ); }) .map(([cat, data]) => { const diffCounts = {}; diff --git a/utils/blindCategories.js b/utils/blindCategories.js new file mode 100644 index 0000000..50a330e --- /dev/null +++ b/utils/blindCategories.js @@ -0,0 +1,136 @@ +/** + * Blind Top 75 카테고리 매핑 + * + * "문제 풀이 현황" 테이블에는 LeetCode 홈페이지 기준 20+개 카테고리 대신 + * Blind Top 75 큐레이션의 10개 카테고리만 노출한다. + * + * 출처: https://www.teamblind.com/post/new-year-gift-curated-list-of-top-75-leetcode-questions-to-save-your-time-oam1oreu + * 관련 이슈: https://github.com/DaleStudy/github/issues/34 + */ + +export const BLIND_CATEGORY_ORDER = [ + "Array", + "Binary", + "Dynamic Programming", + "Graph", + "Interval", + "Linked List", + "Matrix", + "String", + "Tree", + "Heap", +]; + +/** + * 문제 슬러그 → Blind 카테고리 배열. + * + * 한 문제가 여러 Blind 카테고리에 속할 수 있다 (예: merge-k-sorted-lists → Linked List + Heap). + * `problem-categories.json`에 있지만 이 맵에 없는 문제는 Blind 75 밖이므로 집계에서 제외된다. + */ +export const BLIND_PROBLEM_MAP = { + // Array (10) + "two-sum": ["Array"], + "best-time-to-buy-and-sell-stock": ["Array"], + "contains-duplicate": ["Array"], + "product-of-array-except-self": ["Array"], + "maximum-subarray": ["Array"], + "maximum-product-subarray": ["Array"], + "find-minimum-in-rotated-sorted-array": ["Array"], + "search-in-rotated-sorted-array": ["Array"], + "3sum": ["Array"], + "container-with-most-water": ["Array"], + + // Binary (5) + "sum-of-two-integers": ["Binary"], + "number-of-1-bits": ["Binary"], + "counting-bits": ["Binary"], + "missing-number": ["Binary"], + "reverse-bits": ["Binary"], + + // Dynamic Programming (11) + "climbing-stairs": ["Dynamic Programming"], + "coin-change": ["Dynamic Programming"], + "longest-increasing-subsequence": ["Dynamic Programming"], + "longest-common-subsequence": ["Dynamic Programming"], + "word-break": ["Dynamic Programming"], + "combination-sum": ["Dynamic Programming"], + "house-robber": ["Dynamic Programming"], + "house-robber-ii": ["Dynamic Programming"], + "decode-ways": ["Dynamic Programming"], + "unique-paths": ["Dynamic Programming"], + "jump-game": ["Dynamic Programming"], + + // Graph (8) + "clone-graph": ["Graph"], + "course-schedule": ["Graph"], + "pacific-atlantic-water-flow": ["Graph"], + "number-of-islands": ["Graph"], + "longest-consecutive-sequence": ["Graph"], + "alien-dictionary": ["Graph"], + "graph-valid-tree": ["Graph"], + "number-of-connected-components-in-an-undirected-graph": ["Graph"], + + // Interval (5) + "insert-interval": ["Interval"], + "merge-intervals": ["Interval"], + "non-overlapping-intervals": ["Interval"], + "meeting-rooms": ["Interval"], + "meeting-rooms-ii": ["Interval"], + + // Linked List (6; merge-k-sorted-lists 는 Heap 과 중복) + "reverse-linked-list": ["Linked List"], + "linked-list-cycle": ["Linked List"], + "merge-two-sorted-lists": ["Linked List"], + "merge-k-sorted-lists": ["Linked List", "Heap"], + "remove-nth-node-from-end-of-list": ["Linked List"], + "reorder-list": ["Linked List"], + + // Matrix (4) + "set-matrix-zeroes": ["Matrix"], + "spiral-matrix": ["Matrix"], + "rotate-image": ["Matrix"], + "word-search": ["Matrix"], + + // String (10) + "longest-substring-without-repeating-characters": ["String"], + "longest-repeating-character-replacement": ["String"], + "minimum-window-substring": ["String"], + "valid-anagram": ["String"], + "group-anagrams": ["String"], + "valid-parentheses": ["String"], + "valid-palindrome": ["String"], + "longest-palindromic-substring": ["String"], + "palindromic-substrings": ["String"], + "encode-and-decode-strings": ["String"], + + // Tree (14) + "maximum-depth-of-binary-tree": ["Tree"], + "same-tree": ["Tree"], + "invert-binary-tree": ["Tree"], + "binary-tree-maximum-path-sum": ["Tree"], + "binary-tree-level-order-traversal": ["Tree"], + "serialize-and-deserialize-binary-tree": ["Tree"], + "subtree-of-another-tree": ["Tree"], + "construct-binary-tree-from-preorder-and-inorder-traversal": ["Tree"], + "validate-binary-search-tree": ["Tree"], + "kth-smallest-element-in-a-bst": ["Tree"], + "lowest-common-ancestor-of-a-binary-search-tree": ["Tree"], + "implement-trie-prefix-tree": ["Tree"], + "design-add-and-search-words-data-structure": ["Tree"], + "word-search-ii": ["Tree"], + + // Heap (3; merge-k-sorted-lists 는 Linked List 와 중복) + "top-k-frequent-elements": ["Heap"], + "find-median-from-data-stream": ["Heap"], +}; + +/** + * 문제 슬러그에 해당하는 Blind 카테고리 배열을 반환한다. + * Blind 75 밖의 문제는 빈 배열을 반환한다. + * + * @param {string} problemName + * @returns {string[]} + */ +export function getBlindCategories(problemName) { + return BLIND_PROBLEM_MAP[problemName] ?? []; +} From 8fd5a30d1fde6e1d6f5a4229306ff8bf7f9ceb93 Mon Sep 17 00:00:00 2001 From: sounmind Date: Wed, 22 Apr 2026 08:39:22 -0400 Subject: [PATCH 2/2] refactor: read blindCategories from problem-categories.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DaleStudy/leetcode-study#2559 에서 `problem-categories.json` 의 각 문제에 `blindCategories` 필드를 추가한 뒤, Worker 는 이 필드를 직접 읽는다. - utils/blindCategories.js 삭제 (매핑 테이블이 원본 JSON 으로 이전) - BLIND_CATEGORY_ORDER 는 handlers/learning-status.js 내부 상수로 유지 (동률 정렬의 타이브레이커로만 사용) 머지 순서: leetcode-study#2559 먼저, 그 다음 이 PR. Co-Authored-By: Claude Opus 4.7 (1M context) Co-Authored-By: soobing <16860535+soobing@users.noreply.github.com> --- handlers/learning-status.js | 26 ++++-- tests/subrequest-budget.test.js | 1 + utils/blindCategories.js | 136 -------------------------------- 3 files changed, 19 insertions(+), 144 deletions(-) delete mode 100644 utils/blindCategories.js diff --git a/handlers/learning-status.js b/handlers/learning-status.js index 91772ab..7026c51 100644 --- a/handlers/learning-status.js +++ b/handlers/learning-status.js @@ -15,18 +15,29 @@ import { formatLearningStatusComment, upsertLearningStatusComment, } from "../utils/learningComment.js"; -import { - BLIND_CATEGORY_ORDER, - getBlindCategories, -} from "../utils/blindCategories.js"; const MAX_FILE_SIZE = 15000; // 15K 문자 제한 (OpenAI 토큰 안전장치) +// Blind Top 75 큐레이션 순서 — 동률 정렬의 타이브레이커로 사용. +// 출처: https://www.teamblind.com/post/new-year-gift-curated-list-of-top-75-leetcode-questions-to-save-your-time-oam1oreu +const BLIND_CATEGORY_ORDER = [ + "Array", + "Binary", + "Dynamic Programming", + "Graph", + "Interval", + "Linked List", + "Matrix", + "String", + "Tree", + "Heap", +]; + /** * Blind Top 75 카테고리별로 누적 풀이 진행도를 계산한다. * - * `problem-categories.json` 의 LeetCode 세부 카테고리(20+개) 대신 - * Blind 10 카테고리만 표로 노출한다 (이슈 #34). + * `problem-categories.json` 의 각 문제에 포함된 `blindCategories` 필드를 + * 기준으로 Blind 10개 카테고리만 집계한다 (이슈 #34). * * @param {object} categories - problem-categories.json 전체 오브젝트 * @param {string[]} solvedProblems - 사용자가 풀이한 문제 이름 배열 @@ -42,8 +53,7 @@ function buildCategoryProgress(categories, solvedProblems) { ); for (const [problemName, info] of Object.entries(categories)) { - const blindCategories = getBlindCategories(problemName); - for (const cat of blindCategories) { + for (const cat of info.blindCategories) { const entry = categoryMap.get(cat); if (!entry) continue; entry.total++; diff --git a/tests/subrequest-budget.test.js b/tests/subrequest-budget.test.js index e944adc..64883c0 100644 --- a/tests/subrequest-budget.test.js +++ b/tests/subrequest-budget.test.js @@ -118,6 +118,7 @@ describe("subrequest 예산 — 핸들러별 invocation (변경 파일 5개)", ( { difficulty: "Easy", categories: ["Array"], + blindCategories: ["Array"], intended_approach: "Two Pointers", }, ]) diff --git a/utils/blindCategories.js b/utils/blindCategories.js deleted file mode 100644 index 50a330e..0000000 --- a/utils/blindCategories.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Blind Top 75 카테고리 매핑 - * - * "문제 풀이 현황" 테이블에는 LeetCode 홈페이지 기준 20+개 카테고리 대신 - * Blind Top 75 큐레이션의 10개 카테고리만 노출한다. - * - * 출처: https://www.teamblind.com/post/new-year-gift-curated-list-of-top-75-leetcode-questions-to-save-your-time-oam1oreu - * 관련 이슈: https://github.com/DaleStudy/github/issues/34 - */ - -export const BLIND_CATEGORY_ORDER = [ - "Array", - "Binary", - "Dynamic Programming", - "Graph", - "Interval", - "Linked List", - "Matrix", - "String", - "Tree", - "Heap", -]; - -/** - * 문제 슬러그 → Blind 카테고리 배열. - * - * 한 문제가 여러 Blind 카테고리에 속할 수 있다 (예: merge-k-sorted-lists → Linked List + Heap). - * `problem-categories.json`에 있지만 이 맵에 없는 문제는 Blind 75 밖이므로 집계에서 제외된다. - */ -export const BLIND_PROBLEM_MAP = { - // Array (10) - "two-sum": ["Array"], - "best-time-to-buy-and-sell-stock": ["Array"], - "contains-duplicate": ["Array"], - "product-of-array-except-self": ["Array"], - "maximum-subarray": ["Array"], - "maximum-product-subarray": ["Array"], - "find-minimum-in-rotated-sorted-array": ["Array"], - "search-in-rotated-sorted-array": ["Array"], - "3sum": ["Array"], - "container-with-most-water": ["Array"], - - // Binary (5) - "sum-of-two-integers": ["Binary"], - "number-of-1-bits": ["Binary"], - "counting-bits": ["Binary"], - "missing-number": ["Binary"], - "reverse-bits": ["Binary"], - - // Dynamic Programming (11) - "climbing-stairs": ["Dynamic Programming"], - "coin-change": ["Dynamic Programming"], - "longest-increasing-subsequence": ["Dynamic Programming"], - "longest-common-subsequence": ["Dynamic Programming"], - "word-break": ["Dynamic Programming"], - "combination-sum": ["Dynamic Programming"], - "house-robber": ["Dynamic Programming"], - "house-robber-ii": ["Dynamic Programming"], - "decode-ways": ["Dynamic Programming"], - "unique-paths": ["Dynamic Programming"], - "jump-game": ["Dynamic Programming"], - - // Graph (8) - "clone-graph": ["Graph"], - "course-schedule": ["Graph"], - "pacific-atlantic-water-flow": ["Graph"], - "number-of-islands": ["Graph"], - "longest-consecutive-sequence": ["Graph"], - "alien-dictionary": ["Graph"], - "graph-valid-tree": ["Graph"], - "number-of-connected-components-in-an-undirected-graph": ["Graph"], - - // Interval (5) - "insert-interval": ["Interval"], - "merge-intervals": ["Interval"], - "non-overlapping-intervals": ["Interval"], - "meeting-rooms": ["Interval"], - "meeting-rooms-ii": ["Interval"], - - // Linked List (6; merge-k-sorted-lists 는 Heap 과 중복) - "reverse-linked-list": ["Linked List"], - "linked-list-cycle": ["Linked List"], - "merge-two-sorted-lists": ["Linked List"], - "merge-k-sorted-lists": ["Linked List", "Heap"], - "remove-nth-node-from-end-of-list": ["Linked List"], - "reorder-list": ["Linked List"], - - // Matrix (4) - "set-matrix-zeroes": ["Matrix"], - "spiral-matrix": ["Matrix"], - "rotate-image": ["Matrix"], - "word-search": ["Matrix"], - - // String (10) - "longest-substring-without-repeating-characters": ["String"], - "longest-repeating-character-replacement": ["String"], - "minimum-window-substring": ["String"], - "valid-anagram": ["String"], - "group-anagrams": ["String"], - "valid-parentheses": ["String"], - "valid-palindrome": ["String"], - "longest-palindromic-substring": ["String"], - "palindromic-substrings": ["String"], - "encode-and-decode-strings": ["String"], - - // Tree (14) - "maximum-depth-of-binary-tree": ["Tree"], - "same-tree": ["Tree"], - "invert-binary-tree": ["Tree"], - "binary-tree-maximum-path-sum": ["Tree"], - "binary-tree-level-order-traversal": ["Tree"], - "serialize-and-deserialize-binary-tree": ["Tree"], - "subtree-of-another-tree": ["Tree"], - "construct-binary-tree-from-preorder-and-inorder-traversal": ["Tree"], - "validate-binary-search-tree": ["Tree"], - "kth-smallest-element-in-a-bst": ["Tree"], - "lowest-common-ancestor-of-a-binary-search-tree": ["Tree"], - "implement-trie-prefix-tree": ["Tree"], - "design-add-and-search-words-data-structure": ["Tree"], - "word-search-ii": ["Tree"], - - // Heap (3; merge-k-sorted-lists 는 Linked List 와 중복) - "top-k-frequent-elements": ["Heap"], - "find-median-from-data-stream": ["Heap"], -}; - -/** - * 문제 슬러그에 해당하는 Blind 카테고리 배열을 반환한다. - * Blind 75 밖의 문제는 빈 배열을 반환한다. - * - * @param {string} problemName - * @returns {string[]} - */ -export function getBlindCategories(problemName) { - return BLIND_PROBLEM_MAP[problemName] ?? []; -}