diff --git a/src/lib/services/task_results.ts b/src/lib/services/task_results.ts index 4bbac7fa4..cce4582d5 100644 --- a/src/lib/services/task_results.ts +++ b/src/lib/services/task_results.ts @@ -32,7 +32,7 @@ import { NOT_FOUND } from '$lib/constants/http-response-status-codes'; const statusById = await getSubmissionStatusMapWithId(); const statusByName = await getSubmissionStatusMapWithName(); -export async function getTaskResults(userId: string): Promise { +export async function getTaskResults(userId: string | undefined): Promise { // 問題と特定のユーザの回答状況を使ってデータを結合 // 計算量: 問題数をN、特定のユーザの解答数をMとすると、O(N + M)になるはず。 const mergedTasksMap = await getMergedTasksMap(); @@ -127,14 +127,16 @@ async function transferAnswers( // with_mapをtrueにすると、taskIdを使って各TaskResultにO(1)でアクセスできる。 // Why : データ総量を抑えるため。 export async function getTaskResultsOnlyResultExists( - userId: string, + userId: string | undefined, with_map: boolean = false, ): Promise> { const taskResultsMap: Map = new Map(); // TODO: answerの降順にしたい const tasks = await getTasks(); - const answers = await answer_crud.getAnswers(userId); + // Skip the DB round-trip for anonymous users: getAnswers(undefined) drops the + // WHERE filter and full-scans taskAnswer. + const answers = userId !== undefined ? await answer_crud.getAnswers(userId) : new Map(); const tasksHasAnswer = tasks.filter((task) => answers.has(task.task_id)); const taskResultsWithAnswer = tasksHasAnswer.map((task: Task) => { const taskResult = createDefaultTaskResult(userId, task); @@ -215,9 +217,11 @@ export async function getTaskResultsByTaskId( * @param userId - User ID for creating TaskResults * @returns Promise - Array of TaskResult objects */ -async function createTaskResults(tasks: Tasks, userId: string): Promise { - const answers = await answer_crud.getAnswers(userId); +async function createTaskResults(tasks: Tasks, userId: string | undefined): Promise { const isLoggedIn = userId !== undefined; + // Skip the DB round-trip for anonymous users: getAnswers(undefined) drops the + // WHERE filter and full-scans taskAnswer. + const answers = isLoggedIn ? await answer_crud.getAnswers(userId) : new Map(); return tasks.map((task: Task) => { const answer = isLoggedIn ? answers.get(task.task_id) : null; @@ -236,7 +240,7 @@ async function createTaskResults(tasks: Tasks, userId: string): Promise { const tagIds = tagIds_string.split(','); const taskIdByTagIds = await db.taskTag.groupBy({ diff --git a/src/lib/types/task.ts b/src/lib/types/task.ts index 3f09ee387..d32dc6ea3 100644 --- a/src/lib/types/task.ts +++ b/src/lib/types/task.ts @@ -61,8 +61,14 @@ export type TaskGrades = TaskGrade[]; export const taskGradeValues = Object.values(TaskGrade); +/** + * A user's submission result for a single task, extending {@link Task} with status metadata. + * + * Used for both authenticated and anonymous (logged-out) views. + */ export interface TaskResult extends Task { - user_id: string; + /** Owner of this result; `undefined` for anonymous (logged-out) results. */ + user_id: string | undefined; status_name: string; status_id: string; submission_status_image_path: string; diff --git a/src/test/lib/services/task_results.test.ts b/src/test/lib/services/task_results.test.ts index 42d02a79a..23aca1c3d 100644 --- a/src/test/lib/services/task_results.test.ts +++ b/src/test/lib/services/task_results.test.ts @@ -12,8 +12,11 @@ import { describe, test, expect, vi, beforeEach } from 'vitest'; -import { getTaskResults } from '$lib/services/task_results'; -import type { TaskResult, TaskResults } from '$lib/types/task'; +import { getTasks } from '$lib/services/tasks'; +import { getAnswers } from '$lib/services/answers'; +import { getTaskResults, getTaskResultsOnlyResultExists } from '$lib/services/task_results'; + +import type { TaskResult, TaskResults, Tasks } from '$lib/types/task'; import { MOCK_TASKS_DATA, @@ -381,6 +384,27 @@ describe('getTaskResults', () => { }); }); + describe('when anonymous (userId is undefined)', () => { + beforeEach(async () => { + mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS; + vi.mocked(getAnswers).mockClear(); + taskResults = await getTaskResults(undefined); + }); + + test('does not call getAnswers (skips the full-scan DB round-trip)', () => { + expect(getAnswers).not.toHaveBeenCalled(); + }); + + test('returns default (未挑戦) results for all tasks', () => { + expect(taskResults.length).toBeGreaterThan(0); + taskResults.forEach((taskResult: TaskResult) => { + expect(taskResult.is_ac).toBe(false); + expect(taskResult.status_name).toBe('ns'); + expect(taskResult.submission_status_label_name).toBe('未挑戦'); + }); + }); + }); + describe('when answers exist', () => { beforeEach(async () => { mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS; @@ -411,6 +435,33 @@ describe('getTaskResults', () => { }); }); +describe('getTaskResultsOnlyResultExists', () => { + describe('when anonymous (userId is undefined)', () => { + beforeEach(() => { + mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS; + vi.mocked(getAnswers).mockClear(); + vi.mocked(getTasks).mockResolvedValue([ + { + id: '1', + contest_id: 'abc101', + task_id: 'arc099_a', + contest_type: 'ABC', + task_table_index: 'C', + title: 'Minimization', + grade: 'Q3', + }, + ] as unknown as Tasks); + }); + + test('does not call getAnswers and returns an empty array', async () => { + const taskResults = await getTaskResultsOnlyResultExists(undefined, false); + + expect(getAnswers).not.toHaveBeenCalled(); + expect(taskResults).toEqual([]); + }); + }); +}); + describe('mergeTaskAndAnswer', () => { createMergedTaskResults( 'when no answers exist',