diff --git a/handlers/learning-status.js b/handlers/learning-status.js
index 87025e9..b3676ae 100644
--- a/handlers/learning-status.js
+++ b/handlers/learning-status.js
@@ -107,6 +107,8 @@ export async function postLearningStatus(
// 4. 제출 파일별 AI 분석
const submissionResults = [];
+ const totalUsage = { prompt_tokens: 0, completion_tokens: 0 };
+
for (const submission of submissions) {
const problemInfo = categories[submission.problemName];
@@ -145,6 +147,11 @@ export async function postLearningStatus(
openaiApiKey
);
+ if (analysis.usage) {
+ totalUsage.prompt_tokens += analysis.usage.prompt_tokens ?? 0;
+ totalUsage.completion_tokens += analysis.usage.completion_tokens ?? 0;
+ }
+
submissionResults.push({
problemName: submission.problemName,
difficulty: problemInfo.difficulty,
@@ -164,6 +171,8 @@ export async function postLearningStatus(
}
}
+ const hasUsage = totalUsage.prompt_tokens > 0 || totalUsage.completion_tokens > 0;
+
// 5. 카테고리별 진행도 계산
const totalProblems = Object.keys(categories).length;
const categoryProgress = buildCategoryProgress(categories, solvedProblems);
@@ -178,7 +187,14 @@ export async function postLearningStatus(
);
// 7. 댓글 생성 또는 업데이트
- await upsertLearningStatusComment(repoOwner, repoName, prNumber, commentBody, appToken);
+ await upsertLearningStatusComment(
+ repoOwner,
+ repoName,
+ prNumber,
+ commentBody,
+ appToken,
+ hasUsage ? totalUsage : null
+ );
// 8. 결과 반환
const matchedCount = submissionResults.filter((r) => r.matches === true).length;
diff --git a/utils/learningComment.js b/utils/learningComment.js
index 53b77c0..28b34f1 100644
--- a/utils/learningComment.js
+++ b/utils/learningComment.js
@@ -9,6 +9,95 @@ import { getGitHubHeaders } from "./github.js";
*/
const COMMENT_MARKER = "";
+/**
+ * Hidden marker for embedding cumulative usage data in the comment.
+ * Format:
+ */
+const USAGE_DATA_RE = //;
+
+/** gpt-4.1-nano pricing (USD per token) */
+const INPUT_COST_PER_TOKEN = 0.10 / 1_000_000;
+const OUTPUT_COST_PER_TOKEN = 0.40 / 1_000_000;
+
+/**
+ * Calculates cost in USD from token counts.
+ *
+ * @param {number} promptTokens
+ * @param {number} completionTokens
+ * @returns {number}
+ */
+function calcCost(promptTokens, completionTokens) {
+ return promptTokens * INPUT_COST_PER_TOKEN + completionTokens * OUTPUT_COST_PER_TOKEN;
+}
+
+/**
+ * Formats a number with thousand-separating commas.
+ *
+ * @param {number} n
+ * @returns {string}
+ */
+function fmt(n) {
+ return n.toLocaleString("en-US");
+}
+
+/**
+ * Parses per-request usage history embedded in an existing comment body.
+ *
+ * @param {string|undefined} body
+ * @returns {Array<{ prompt: number, completion: number }>}
+ */
+function parseUsageFromComment(body) {
+ if (!body) return [];
+ const match = body.match(USAGE_DATA_RE);
+ if (!match) return [];
+ try {
+ const parsed = JSON.parse(match[1]);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch {
+ return [];
+ }
+}
+
+/**
+ * Builds the usage footer section (per-request history table + hidden data marker).
+ *
+ * @param {Array<{ prompt: number, completion: number }>} history - all requests including current (latest last)
+ * @returns {string}
+ */
+function formatUsageSection(history) {
+ const lines = [];
+ lines.push("");
+ lines.push("🔢 API 사용량 (gpt-4.1-nano)
");
+ lines.push("");
+ lines.push("| 요청 | 입력 토큰 | 출력 토큰 | 합계 | 비용 |");
+ lines.push("|---:|---:|---:|---:|---:|");
+
+ let totalPrompt = 0;
+ let totalCompletion = 0;
+
+ for (let i = 0; i < history.length; i++) {
+ const { prompt, completion } = history[i];
+ const total = prompt + completion;
+ const cost = calcCost(prompt, completion);
+ lines.push(`| #${i + 1} | ${fmt(prompt)} | ${fmt(completion)} | ${fmt(total)} | $${cost.toFixed(6)} |`);
+ totalPrompt += prompt;
+ totalCompletion += completion;
+ }
+
+ if (history.length > 1) {
+ const totalCost = calcCost(totalPrompt, totalCompletion);
+ lines.push(`| **합계** | **${fmt(totalPrompt)}** | **${fmt(totalCompletion)}** | **${fmt(totalPrompt + totalCompletion)}** | **$${totalCost.toFixed(6)}** |`);
+ }
+
+ lines.push("");
+ lines.push(" ");
+
+ // Embed history for future parsing
+ lines.push(``);
+
+ return lines.join("\n");
+}
+
/**
* Returns true when `comment` is a Bot comment containing our marker.
*
@@ -129,11 +218,15 @@ export function formatLearningStatusComment(
* Searches through the first 100 issue comments for an existing Bot comment
* that contains COMMENT_MARKER. If found, PATCHes it; otherwise POSTs a new one.
*
+ * When `currentUsage` is provided, appends a collapsible usage/cost section to
+ * the comment and accumulates it with any previously stored cumulative totals.
+ *
* @param {string} repoOwner
* @param {string} repoName
* @param {number} prNumber
* @param {string} commentBody
* @param {string} appToken
+ * @param {{ prompt_tokens: number, completion_tokens: number }|null} [currentUsage]
* @returns {Promise}
*/
export async function upsertLearningStatusComment(
@@ -141,7 +234,8 @@ export async function upsertLearningStatusComment(
repoName,
prNumber,
commentBody,
- appToken
+ appToken,
+ currentUsage = null
) {
const baseUrl = `https://api.github.com/repos/${repoOwner}/${repoName}`;
@@ -160,6 +254,16 @@ export async function upsertLearningStatusComment(
const comments = await listResponse.json();
const existing = comments.find(isLearningStatusComment);
+ // Append usage section when token data is available
+ if (currentUsage) {
+ const prevHistory = parseUsageFromComment(existing?.body);
+ const history = [
+ ...prevHistory,
+ { prompt: currentUsage.prompt_tokens, completion: currentUsage.completion_tokens },
+ ];
+ commentBody = commentBody.trimEnd() + "\n\n" + formatUsageSection(history) + "\n";
+ }
+
if (existing) {
// Update existing comment
console.log(
diff --git a/utils/openai.js b/utils/openai.js
index cee3539..bc8f7ee 100644
--- a/utils/openai.js
+++ b/utils/openai.js
@@ -254,5 +254,6 @@ ${truncatedContent}
return {
matches: parsed.matches === true,
explanation: typeof parsed.explanation === "string" ? parsed.explanation : "",
+ usage: data.usage ?? null,
};
}