Skip to content

Commit 99cb542

Browse files
soobingsounmindclaude
committed
fix: 기수 프로젝트 기반으로 학습 현황 집계 범위 수정
재참여자의 이전 기수 풀이가 현재 기수 누적 학습 현황에 포함되던 문제를 수정한다. 레포 전체 트리 스캔 대신, 열린 "리트코드 스터디X기" GitHub 프로젝트에 연결된 머지된 PR만을 기준으로 집계하도록 변경한다. Closes #10 Co-Authored-By: sounmind <37020415+sounmind@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6768293 commit 99cb542

2 files changed

Lines changed: 192 additions & 4 deletions

File tree

handlers/learning-status.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import {
99
fetchProblemCategories,
10-
fetchUserSolutions,
10+
fetchCohortUserSolutions,
1111
fetchPRSubmissions,
1212
} from "../utils/learningData.js";
1313
import { generateApproachAnalysis } from "../utils/openai.js";
@@ -88,10 +88,10 @@ export async function postLearningStatus(
8888
return { skipped: "no-categories-file" };
8989
}
9090

91-
// 2. 사용자의 누적 풀이 목록 조회
92-
const solvedProblems = await fetchUserSolutions(repoOwner, repoName, username, appToken);
91+
// 2. 이번 기수에서 사용자가 제출한 풀이 목록 조회
92+
const solvedProblems = await fetchCohortUserSolutions(repoOwner, repoName, username, appToken);
9393
console.log(
94-
`[learningStatus] PR #${prNumber}: ${username} has ${solvedProblems.length} cumulative solutions`
94+
`[learningStatus] PR #${prNumber}: ${username} has ${solvedProblems.length} solutions in current cohort`
9595
);
9696

9797
// 3. 이번 PR 제출 파일 목록 조회

utils/learningData.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,194 @@
44

55
import { getGitHubHeaders } from "./github.js";
66

7+
const GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
8+
const COHORT_PROJECT_PATTERN = / \s*\d+/;
9+
10+
/**
11+
* GitHub GraphQL API 호출 헬퍼
12+
*
13+
* @param {string} query
14+
* @param {string} appToken
15+
* @returns {Promise<object>}
16+
*/
17+
async function graphql(query, appToken) {
18+
const response = await fetch(GITHUB_GRAPHQL_URL, {
19+
method: "POST",
20+
headers: {
21+
...getGitHubHeaders(appToken),
22+
"Content-Type": "application/json",
23+
},
24+
body: JSON.stringify({ query }),
25+
});
26+
27+
if (!response.ok) {
28+
throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
29+
}
30+
31+
const result = await response.json();
32+
if (result.errors) {
33+
throw new Error(`GraphQL error: ${JSON.stringify(result.errors)}`);
34+
}
35+
36+
return result.data;
37+
}
38+
39+
/**
40+
* 현재 진행 중인 기수 프로젝트 ID를 조회한다.
41+
* "리트코드 스터디X기" 패턴의 열린 프로젝트를 찾는다.
42+
*
43+
* @param {string} repoOwner
44+
* @param {string} repoName
45+
* @param {string} appToken
46+
* @returns {Promise<string|null>} 프로젝트 node ID, 없으면 null
47+
*/
48+
async function fetchActiveCohortProjectId(repoOwner, repoName, appToken) {
49+
const data = await graphql(
50+
`{
51+
repository(owner: "${repoOwner}", name: "${repoName}") {
52+
projectsV2(first: 20) {
53+
nodes {
54+
id
55+
title
56+
closed
57+
}
58+
}
59+
}
60+
}`,
61+
appToken
62+
);
63+
64+
const projects = data.repository.projectsV2.nodes;
65+
const active = projects.find(
66+
(p) => !p.closed && COHORT_PROJECT_PATTERN.test(p.title)
67+
);
68+
69+
if (!active) {
70+
console.warn(
71+
`[fetchActiveCohortProjectId] No open cohort project found for ${repoOwner}/${repoName}`
72+
);
73+
return null;
74+
}
75+
76+
console.log(
77+
`[fetchActiveCohortProjectId] Active cohort project: "${active.title}" (${active.id})`
78+
);
79+
return active.id;
80+
}
81+
82+
/**
83+
* 기수 프로젝트에서 해당 유저가 머지한 PR 번호 목록을 반환한다.
84+
* 프로젝트 아이템을 페이지네이션하며 author.login으로 필터링한다.
85+
*
86+
* @param {string} projectId
87+
* @param {string} username
88+
* @param {string} appToken
89+
* @returns {Promise<number[]>}
90+
*/
91+
async function fetchUserMergedPRsInProject(projectId, username, appToken) {
92+
const prNumbers = [];
93+
let cursor = null;
94+
95+
while (true) {
96+
const afterClause = cursor ? `, after: "${cursor}"` : "";
97+
const data = await graphql(
98+
`{
99+
node(id: "${projectId}") {
100+
... on ProjectV2 {
101+
items(first: 100${afterClause}) {
102+
pageInfo { hasNextPage endCursor }
103+
nodes {
104+
content {
105+
... on PullRequest {
106+
number
107+
state
108+
author { login }
109+
}
110+
}
111+
}
112+
}
113+
}
114+
}
115+
}`,
116+
appToken
117+
);
118+
119+
const { nodes, pageInfo } = data.node.items;
120+
121+
for (const item of nodes) {
122+
const pr = item.content;
123+
if (
124+
pr?.state === "MERGED" &&
125+
pr?.author?.login?.toLowerCase() === username.toLowerCase()
126+
) {
127+
prNumbers.push(pr.number);
128+
}
129+
}
130+
131+
if (!pageInfo.hasNextPage) break;
132+
cursor = pageInfo.endCursor;
133+
}
134+
135+
return prNumbers;
136+
}
137+
138+
/**
139+
* 현재 기수 프로젝트에서 해당 유저가 제출한 문제 목록을 반환한다.
140+
*
141+
* 기수 프로젝트를 찾지 못하면 전체 레포 트리 스캔(fetchUserSolutions)으로 폴백한다.
142+
*
143+
* @param {string} repoOwner
144+
* @param {string} repoName
145+
* @param {string} username
146+
* @param {string} appToken
147+
* @returns {Promise<string[]>}
148+
*/
149+
export async function fetchCohortUserSolutions(
150+
repoOwner,
151+
repoName,
152+
username,
153+
appToken
154+
) {
155+
const projectId = await fetchActiveCohortProjectId(
156+
repoOwner,
157+
repoName,
158+
appToken
159+
);
160+
161+
if (!projectId) {
162+
console.warn(
163+
`[fetchCohortUserSolutions] Falling back to full tree scan for ${username}`
164+
);
165+
return fetchUserSolutions(repoOwner, repoName, username, appToken);
166+
}
167+
168+
const prNumbers = await fetchUserMergedPRsInProject(
169+
projectId,
170+
username,
171+
appToken
172+
);
173+
174+
console.log(
175+
`[fetchCohortUserSolutions] ${username} has ${prNumbers.length} merged PRs in current cohort`
176+
);
177+
178+
const problemNames = new Set();
179+
for (const prNumber of prNumbers) {
180+
const submissions = await fetchPRSubmissions(
181+
repoOwner,
182+
repoName,
183+
prNumber,
184+
username,
185+
appToken
186+
);
187+
for (const { problemName } of submissions) {
188+
problemNames.add(problemName);
189+
}
190+
}
191+
192+
return Array.from(problemNames);
193+
}
194+
7195
/**
8196
* Fetches problem-categories.json from the repo root via GitHub API.
9197
* Returns parsed JSON object, or null if the file is not found (404).

0 commit comments

Comments
 (0)