Skip to content

Commit 1c12f7d

Browse files
committed
Revert "Merge pull request #15 from DaleStudy/perf/10-reduce-subrequests"
This reverts commit a430e96, reversing changes made to d51353a.
1 parent a430e96 commit 1c12f7d

3 files changed

Lines changed: 49 additions & 171 deletions

File tree

handlers/learning-status.js

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
fetchCohortUserSolutions,
1111
fetchPRSubmissions,
1212
} from "../utils/learningData.js";
13-
import { generateBatchApproachAnalysis } from "../utils/openai.js";
13+
import { generateApproachAnalysis } from "../utils/openai.js";
1414
import {
1515
formatLearningStatusComment,
1616
upsertLearningStatusComment,
@@ -105,10 +105,9 @@ export async function postLearningStatus(
105105
`[learningStatus] PR #${prNumber}: analyzing ${submissions.length} submission(s) for ${username}`
106106
);
107107

108-
// 4. 제출 파일 코드 다운로드 (N회 fetch)
108+
// 4. 제출 파일별 AI 분석
109109
const submissionResults = [];
110-
const batchItems = [];
111-
const batchIndices = [];
110+
const totalUsage = { prompt_tokens: 0, completion_tokens: 0 };
112111

113112
for (const submission of submissions) {
114113
const problemInfo = categories[submission.problemName];
@@ -127,6 +126,7 @@ export async function postLearningStatus(
127126
}
128127

129128
try {
129+
// 파일 원본 내용 가져오기
130130
const rawResponse = await fetch(submission.rawUrl);
131131
if (!rawResponse.ok) {
132132
throw new Error(`Failed to fetch raw file: ${rawResponse.status} ${rawResponse.statusText}`);
@@ -140,18 +140,27 @@ export async function postLearningStatus(
140140
);
141141
}
142142

143-
const idx = submissionResults.length;
143+
const analysis = await generateApproachAnalysis(
144+
fileContent,
145+
submission.problemName,
146+
problemInfo,
147+
openaiApiKey
148+
);
149+
150+
if (analysis.usage) {
151+
totalUsage.prompt_tokens += analysis.usage.prompt_tokens ?? 0;
152+
totalUsage.completion_tokens += analysis.usage.completion_tokens ?? 0;
153+
}
154+
144155
submissionResults.push({
145156
problemName: submission.problemName,
146157
difficulty: problemInfo.difficulty,
147-
matches: null,
148-
explanation: "",
158+
matches: analysis.matches,
159+
explanation: analysis.explanation,
149160
});
150-
batchItems.push({ problemName: submission.problemName, fileContent, problemInfo });
151-
batchIndices.push(idx);
152161
} catch (error) {
153162
console.error(
154-
`[learningStatus] Failed to fetch "${submission.problemName}": ${error.message}`
163+
`[learningStatus] Failed to analyze "${submission.problemName}": ${error.message}`
155164
);
156165
submissionResults.push({
157166
problemName: submission.problemName,
@@ -162,33 +171,13 @@ export async function postLearningStatus(
162171
}
163172
}
164173

165-
// 5. AI 일괄 분석 (1회 OpenAI 호출로 모든 제출 파일 분석)
166-
let totalUsage = null;
167-
if (batchItems.length > 0) {
168-
try {
169-
console.log(
170-
`[learningStatus] PR #${prNumber}: batch analyzing ${batchItems.length} file(s) via OpenAI`
171-
);
172-
const { results: batchResults, usage } = await generateBatchApproachAnalysis(batchItems, openaiApiKey);
173-
totalUsage = usage;
174-
for (let i = 0; i < batchResults.length; i++) {
175-
submissionResults[batchIndices[i]].matches = batchResults[i].matches;
176-
submissionResults[batchIndices[i]].explanation = batchResults[i].explanation;
177-
}
178-
} catch (error) {
179-
console.error(
180-
`[learningStatus] Batch analysis failed: ${error.message}`
181-
);
182-
}
183-
}
184-
185-
const hasUsage = totalUsage != null;
174+
const hasUsage = totalUsage.prompt_tokens > 0 || totalUsage.completion_tokens > 0;
186175

187-
// 6. 카테고리별 진행도 계산
176+
// 5. 카테고리별 진행도 계산
188177
const totalProblems = Object.keys(categories).length;
189178
const categoryProgress = buildCategoryProgress(categories, solvedProblems);
190179

191-
// 7. 댓글 본문 포맷
180+
// 6. 댓글 본문 포맷
192181
const commentBody = formatLearningStatusComment(
193182
username,
194183
submissionResults,
@@ -197,7 +186,7 @@ export async function postLearningStatus(
197186
categoryProgress
198187
);
199188

200-
// 8. 댓글 생성 또는 업데이트
189+
// 7. 댓글 생성 또는 업데이트
201190
await upsertLearningStatusComment(
202191
repoOwner,
203192
repoName,
@@ -207,7 +196,7 @@ export async function postLearningStatus(
207196
hasUsage ? totalUsage : null
208197
);
209198

210-
// 9. 결과 반환
199+
// 8. 결과 반환
211200
const matchedCount = submissionResults.filter((r) => r.matches === true).length;
212201
return { analyzed: submissionResults.length, matched: matchedCount };
213202
}

utils/learningData.js

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,16 @@ async function fetchActiveCohortProjectId(repoOwner, repoName, appToken) {
8080
}
8181

8282
/**
83-
* 기수 프로젝트의 아이템을 페이지네이션하며 해당 유저가 머지한 PR의
84-
* 파일 경로를 GraphQL로 한 번에 조회하여 풀이한 문제 이름 목록을 반환한다.
85-
*
86-
* PR별 REST 호출 없이 GraphQL 응답에 files를 포함시켜 subrequest를 절약한다.
83+
* 기수 프로젝트에서 해당 유저가 머지한 PR 번호 목록을 반환한다.
84+
* 프로젝트 아이템을 페이지네이션하며 author.login으로 필터링한다.
8785
*
8886
* @param {string} projectId
8987
* @param {string} username
9088
* @param {string} appToken
91-
* @returns {Promise<string[]>}
89+
* @returns {Promise<number[]>}
9290
*/
93-
async function fetchCohortSolvedFromProject(projectId, username, appToken) {
94-
const usernamePattern = new RegExp(
95-
`^([^/]+)/${escapeRegExp(username)}\\.[^/]+$`
96-
);
97-
const problemNames = new Set();
91+
async function fetchUserMergedPRsInProject(projectId, username, appToken) {
92+
const prNumbers = [];
9893
let cursor = null;
9994

10095
while (true) {
@@ -108,11 +103,9 @@ async function fetchCohortSolvedFromProject(projectId, username, appToken) {
108103
nodes {
109104
content {
110105
... on PullRequest {
106+
number
111107
state
112108
author { login }
113-
files(first: 100) {
114-
nodes { path }
115-
}
116109
}
117110
}
118111
}
@@ -131,20 +124,15 @@ async function fetchCohortSolvedFromProject(projectId, username, appToken) {
131124
pr?.state === "MERGED" &&
132125
pr?.author?.login?.toLowerCase() === username.toLowerCase()
133126
) {
134-
for (const file of pr.files?.nodes || []) {
135-
const match = file.path.match(usernamePattern);
136-
if (match) {
137-
problemNames.add(match[1]);
138-
}
139-
}
127+
prNumbers.push(pr.number);
140128
}
141129
}
142130

143131
if (!pageInfo.hasNextPage) break;
144132
cursor = pageInfo.endCursor;
145133
}
146134

147-
return Array.from(problemNames);
135+
return prNumbers;
148136
}
149137

150138
/**
@@ -177,17 +165,31 @@ export async function fetchCohortUserSolutions(
177165
return fetchUserSolutions(repoOwner, repoName, username, appToken);
178166
}
179167

180-
const problems = await fetchCohortSolvedFromProject(
168+
const prNumbers = await fetchUserMergedPRsInProject(
181169
projectId,
182170
username,
183171
appToken
184172
);
185173

186174
console.log(
187-
`[fetchCohortUserSolutions] ${username} solved ${problems.length} problems in current cohort`
175+
`[fetchCohortUserSolutions] ${username} has ${prNumbers.length} merged PRs in current cohort`
188176
);
189177

190-
return problems;
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);
191193
}
192194

193195
/**

utils/openai.js

Lines changed: 0 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -258,119 +258,6 @@ ${truncatedContent}
258258
};
259259
}
260260

261-
/**
262-
* 여러 솔루션 파일의 접근법 일치 여부를 한 번의 API 호출로 일괄 분석.
263-
* subrequest 수를 줄이기 위해 파일당 개별 호출 대신 배치로 처리한다.
264-
*
265-
* @param {Array<{problemName: string, fileContent: string, problemInfo: object}>} items
266-
* @param {string} apiKey - OpenAI API 키
267-
* @returns {Promise<{results: Array<{matches: boolean, explanation: string}>, usage: object|null}>}
268-
*/
269-
export async function generateBatchApproachAnalysis(items, apiKey) {
270-
if (items.length === 0) return { results: [], usage: null };
271-
272-
// 단건이면 기존 함수 위임
273-
if (items.length === 1) {
274-
const { fileContent, problemName, problemInfo } = items[0];
275-
const result = await generateApproachAnalysis(fileContent, problemName, problemInfo, apiKey);
276-
return {
277-
results: [{ matches: result.matches, explanation: result.explanation }],
278-
usage: result.usage ?? null,
279-
};
280-
}
281-
282-
const systemPrompt = `You are an algorithm analysis expert. You will receive multiple problems. For each one, determine if the submitted code matches the intended approach.
283-
284-
Respond with a JSON object containing a "results" array with exactly ${items.length} entries, in the same order as the input:
285-
{
286-
"results": [
287-
{ "matches": true, "explanation": "한국어 1문장, 80자 이내" },
288-
...
289-
]
290-
}
291-
292-
Rules:
293-
- matches=true if the core data structure or algorithm matches the intended approach
294-
- matches=false if brute force was used when an optimized approach was intended
295-
- Keep each explanation to 1 sentence in Korean, 80 characters or fewer
296-
- You MUST return exactly ${items.length} results`;
297-
298-
const MAX_BATCH_FILE_SIZE = 5000;
299-
300-
const problemSections = items.map(({ problemName, fileContent, problemInfo }, i) => {
301-
const truncated = fileContent.slice(0, MAX_BATCH_FILE_SIZE);
302-
return `## 문제 ${i + 1}: ${problemName}
303-
- 난이도: ${problemInfo.difficulty}
304-
- 카테고리: ${(problemInfo.categories || []).join(", ")}
305-
- 의도된 접근법: ${problemInfo.intended_approach}
306-
307-
\`\`\`
308-
${truncated}
309-
\`\`\``;
310-
});
311-
312-
const userPrompt = problemSections.join("\n\n") +
313-
`\n\n위 ${items.length}개 코드가 각각 의도된 접근법과 일치하는지 분석해주세요.`;
314-
315-
const response = await fetch("https://api.openai.com/v1/chat/completions", {
316-
method: "POST",
317-
headers: {
318-
Authorization: `Bearer ${apiKey}`,
319-
"Content-Type": "application/json",
320-
},
321-
body: JSON.stringify({
322-
model: "gpt-4.1-nano",
323-
messages: [
324-
{ role: "system", content: systemPrompt },
325-
{ role: "user", content: userPrompt },
326-
],
327-
response_format: { type: "json_object" },
328-
max_tokens: 200 * items.length,
329-
temperature: 0.2,
330-
}),
331-
});
332-
333-
if (!response.ok) {
334-
const error = await response.text();
335-
throw new Error(`OpenAI batch API error: ${error}`);
336-
}
337-
338-
const data = await response.json();
339-
const content = data.choices[0]?.message?.content;
340-
341-
if (!content) {
342-
throw new Error("Empty response from OpenAI batch analysis");
343-
}
344-
345-
let parsed;
346-
try {
347-
parsed = JSON.parse(content);
348-
} catch {
349-
throw new Error(`OpenAI returned invalid JSON: ${content.slice(0, 200)}`);
350-
}
351-
352-
const rawResults = parsed.results;
353-
if (!Array.isArray(rawResults)) {
354-
throw new Error(`OpenAI did not return a results array`);
355-
}
356-
357-
if (rawResults.length !== items.length) {
358-
console.warn(
359-
`[generateBatchApproachAnalysis] Expected ${items.length} results, got ${rawResults.length}`
360-
);
361-
}
362-
363-
const results = items.map((_, i) => {
364-
const r = rawResults[i];
365-
return {
366-
matches: r?.matches === true,
367-
explanation: typeof r?.explanation === "string" ? r.explanation : "",
368-
};
369-
});
370-
371-
return { results, usage: data.usage ?? null };
372-
}
373-
374261
/**
375262
* 솔루션의 시간/공간 복잡도 분석.
376263
* 사용자가 코드 어딘가에 자유 포맷으로 남긴 TC/SC 주석을 함께 추출하여

0 commit comments

Comments
 (0)