Skip to content

Commit 2a47983

Browse files
authored
Merge pull request #13 from DaleStudy/fix/10-cohort-based-learning-status
fix: 기수 프로젝트 기반으로 학습 현황 집계 범위 수정
2 parents 79358ee + 99cb542 commit 2a47983

File tree

2 files changed

+192
-4
lines changed

2 files changed

+192
-4
lines changed

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)