From 43dc8f29d400b71d53d8d581e2470ae933ca0f2d Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Feb 2026 10:10:01 +0000 Subject: [PATCH 1/2] feat: Add and update table for ACL (#3120) --- .../add-and-update-acl-providers/plan.md | 91 +++++++++++++ .../how-to-add-contest-table-provider.md | 64 ++++++++-- prisma/tasks.ts | 42 ++++++ src/lib/utils/contest.ts | 1 + src/lib/utils/contest_table_provider.ts | 83 +++++++++++- .../lib/utils/contest_table_provider.test.ts | 120 ++++++++++++++++++ .../test_cases/contest_table_provider.ts | 21 +++ src/test/lib/utils/test_cases/contest_type.ts | 4 + 8 files changed, 410 insertions(+), 16 deletions(-) create mode 100644 docs/dev-notes/2026-02-01/add-and-update-acl-providers/plan.md diff --git a/docs/dev-notes/2026-02-01/add-and-update-acl-providers/plan.md b/docs/dev-notes/2026-02-01/add-and-update-acl-providers/plan.md new file mode 100644 index 000000000..82ce8a81d --- /dev/null +++ b/docs/dev-notes/2026-02-01/add-and-update-acl-providers/plan.md @@ -0,0 +1,91 @@ +# 計画: ACLBeginnerProvider と ACLProvider を追加 (#3120) + +## 概要 + +既存の `ACLPracticeProvider` グループの下に `ACLBeginnerProvider` と `ACLProvider` を追加し、ACL Beginner Contest と ACL Contest 1 に対応させる。 + +## 変更内容 + +### 1. `src/lib/utils/contest_table_provider.ts` に Provider クラスを追加 + +**ACLPracticeProvider の位置**: 770行目以降に、以下の 2 つのクラスを順に追加: + +- `ACLBeginnerProvider` (ContestType.ABC_LIKE、contest_id: 'abl') +- `ACLProvider` (ContestType.ARC_LIKE、contest_id: 'acl1') + +参考: [how-to-add-contest-table-provider.md - パターン2](../../guides/how-to-add-contest-table-provider.md#パターン2-単一ソース型edpc--tdpc--fps_24--acl_practice) + +**主な設定**: + +- 両者とも contest_id による文字列比較でフィルタリング +- getDisplayConfig: `tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-2'` +- getContestRoundLabel: 空文字列を返す +- 両者とも taskIndex を表示(isShownTaskIndex: true) + +### 2. `prepareContestProviderPresets()` を更新(1214行目付近) + +**変更前**: + +``` +AclPractice: () => + new ContestTableProviderGroup(`AtCoder Library Practice Contest`, { + buttonLabel: 'ACL Practice', + ariaLabel: 'Filter ACL Practice Contest', + }).addProvider(new ACLPracticeProvider(ContestType.ACL_PRACTICE)), +``` + +**変更後**: `Acl` に名前変更し、3 つのプロバイダすべてを追加 + +``` +Acl: () => + new ContestTableProviderGroup(`AtCoder Library Contests`, { + buttonLabel: 'ACL', + ariaLabel: 'Filter ACL Contests', + }).addProviders( + new ACLPracticeProvider(ContestType.ACL_PRACTICE), + new ACLBeginnerProvider(ContestType.ABC_LIKE), + new ACLProvider(ContestType.ARC_LIKE), + ), +``` + +### 3. `contestTableProviderGroups` を更新(1250行目付近) + +**変更前**: + +``` +aclPractice: prepareContestProviderPresets().AclPractice(), +``` + +**変更後**: + +``` +acl: prepareContestProviderPresets().Acl(), +``` + +### 4. `src/test/lib/utils/test_cases/contest_table_provider.ts` にテストデータを追加 + +`createContestTasks()` を使用してモックデータを作成: + +- `taskResultsForACLBeginnerProvider`: contest_id 'abl' の 6 つのタスク(A~F) +- `taskResultsForACLProvider`: contest_id 'acl1' の 6 つのタスク(A~F) + +(prisma/tasks.ts の既存タスク定義を使用) + +### 5. `src/test/lib/utils/contest_table_provider.test.ts` にテストを追加 + +各新規プロバイダについて、以下の項目をカバーするテストを追加: + +- getMetadata() の検証(title、abbreviationName) +- getDisplayConfig() の検証 +- filter() の動作確認 +- generateTable() のテーブル構造 +- getContestRoundIds() と getHeaderIdsForTask() +- getContestRoundLabel() が空文字列を返すことを検証 + +--- + +## 参考資料 + +- 実装パターン: [how-to-add-contest-table-provider.md - パターン2](../../guides/how-to-add-contest-table-provider.md#パターン2-単一ソース型edpc--tdpc--fps_24--acl_practice) +- GitHub Issue: #3120 +- prisma/tasks.ts の既存データ(abl: 6 問、acl1: 6 問) diff --git a/docs/guides/how-to-add-contest-table-provider.md b/docs/guides/how-to-add-contest-table-provider.md index b71ccae8e..591e86522 100644 --- a/docs/guides/how-to-add-contest-table-provider.md +++ b/docs/guides/how-to-add-contest-table-provider.md @@ -10,6 +10,48 @@ --- +## 0. 実装前確認フェーズ + +新しい Provider を実装する前に、必ず以下の事項を確認してください。 + +### 事前確認チェックリスト + +- [ ] **ContestType の選択** + - 新規追加が必要か? → `src/lib/types/contest.ts` を確認 + - 既存の `ContestType` で対応できないか? → 「各コンテスト種別の特有仕様」を参照 + - 判断基準: 複数の異なる contest_id を統一表示するなら「複合型」の可能性 + +- [ ] **データ存在確認** + - `prisma/tasks.ts` に該当 `contest_id` のタスクが存在するか確認 + - 複合型の場合は `prisma/contest_task_pairs.ts` で共有問題を確認 + +- [ ] **実装パターン判定** + - **パターン1(範囲フィルタ型)**: ABC 001~041、ARC 058~103 など → 数値範囲でフィルタ + - **パターン2(単一ソース型)**: EDPC、TDPC、ACL_PRACTICE など → 単一 contest_id のみ + - **パターン3(複合ソース型)**: ABS、ABC-Like など → 複数 contest_id を統一表示 + - 対応セクション: [実装パターン](#実装パターン) + +- [ ] **ガイドの実装例の確認** + - 判定したパターンの実装例を確認してテンプレート理解 + - モック設定時に必要な `classifyContest()` の戻り値を確認 + +### 記入例 + +```markdown +**新規 Provider 名**: ACLBeginnerProvider + +事前確認結果: + +- ContestType: ABC_LIKE(既存流用) +- contest_id: abl(prisma/tasks.ts に 6 つのタスク存在確認) +- パターン: パターン2(単一ソース型) +- テンプレート: EDPC と同一構造 + +→ 実装フェーズ開始 +``` + +--- + ## Test Driven Development (TDD) 設計ガイド 新しい Provider を実装する際は、**テストファースト** のアプローチを推奨します。 @@ -207,12 +249,16 @@ class TessokuBookSectionProvider extends TessokuBookProvider { ### 単一ソース型 -| コンテスト | contest_id | セクション | フォーマット | -| ------------ | ------------- | ---------- | ------------ | -| EDPC | `'dp'` | 26問 | A~Z | -| TDPC | `'tdpc'` | 26問 | A~Z | -| FPS_24 | `'fps-24'` | 24問 | A~X | -| ACL_PRACTICE | `'practice2'` | 12問 | A~L | +| コンテスト | contest_id | セクション | フォーマット | +| -------------- | ------------- | ---------- | ------------ | +| EDPC | `'dp'` | 26問 | A~Z | +| TDPC | `'tdpc'` | 26問 | A~Z | +| FPS_24 | `'fps-24'` | 24問 | A~X | +| ACL_PRACTICE | `'practice2'` | 12問 | A~L | +| ACL_BEGINNER\* | `'abl'` | 6問 | A~F | +| ACL_CONTEST1\* | `'acl1'` | 6問 | A~F | + +\*注: ACL_PRACTICE、ACL_BEGINNER、ACL_CONTEST1 は `Acl` グループの下で 3 つのコンテストが統一管理されています。 ### 複合ソース型 @@ -419,11 +465,11 @@ describe('CustomProvider with unique config', () => { - [#2835](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2835) - ARC104OnwardsProvider - [#2837](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2837) - AGC001OnwardsProvider - [#2838](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2838) - ABC001~041 & ARC001~057 -- [#2840](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2840) - ABCLikeProvider +- [#2840](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2840)、[#3108](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3108) - ABCLikeProvider - [#2776](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2776) - TessokuBookProvider - [#2785](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2785) - MathAndAlgorithmProvider - [#2797](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2797) - FPS24Provider -- [#2920](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2920) - ACLPracticeProvider +- [#2920](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/2920)、[#3120](https://github.com/AtCoder-NoviSteps/AtCoderNoviSteps/issues/3120) - ACLPracticeProvider、ACLBeginnerProvider、ACLProvider ### 実装ファイル @@ -433,4 +479,4 @@ describe('CustomProvider with unique config', () => { --- -**最終更新**: 2026-01-26 +**最終更新**: 2026-02-01 diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 574eeede4..53364e4cc 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -7976,6 +7976,48 @@ export const tasks = [ name: 'Accepted...?', title: 'A. Accepted...?', }, + { + id: 'acl1_f', + contest_id: 'acl1', + problem_index: 'F', + name: 'Center Rearranging', + title: 'F. Center Rearranging', + }, + { + id: 'acl1_e', + contest_id: 'acl1', + problem_index: 'E', + name: 'Shuffle Window', + title: 'E. Shuffle Window', + }, + { + id: 'acl1_d', + contest_id: 'acl1', + problem_index: 'D', + name: 'Keep Distances', + title: 'D. Keep Distances', + }, + { + id: 'acl1_c', + contest_id: 'acl1', + problem_index: 'C', + name: 'Moving Pieces', + title: 'C. Moving Pieces', + }, + { + id: 'acl1_b', + contest_id: 'acl1', + problem_index: 'B', + name: 'Sum is Multiple', + title: 'B. Sum is Multiple', + }, + { + id: 'acl1_a', + contest_id: 'acl1', + problem_index: 'A', + name: 'Reachable Towns', + title: 'A. Reachable Towns', + }, { id: 'tenka1_2019_f', contest_id: 'tenka1-2019', diff --git a/src/lib/utils/contest.ts b/src/lib/utils/contest.ts index 26c239e1f..0e4f8f0a8 100644 --- a/src/lib/utils/contest.ts +++ b/src/lib/utils/contest.ts @@ -130,6 +130,7 @@ const ARC_LIKE: ContestPrefix = { keyence2021: 'キーエンス プログラミング コンテスト 2021', 'jsc2019-qual': '第一回日本最強プログラマー学生選手権-予選-', 'nikkei2019-qual': '全国統一プログラミング王決定戦予選', + acl1: 'ACL Contest 1', } as const; const arcLikePrefixes = new Set(getContestPrefixes(ARC_LIKE)); diff --git a/src/lib/utils/contest_table_provider.ts b/src/lib/utils/contest_table_provider.ts index 13c8e5a1d..a7f974d6e 100644 --- a/src/lib/utils/contest_table_provider.ts +++ b/src/lib/utils/contest_table_provider.ts @@ -796,6 +796,70 @@ export class ACLPracticeProvider extends ContestTableProviderBase { } } +export class ACLBeginnerProvider extends ContestTableProviderBase { + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } + return taskResult.contest_id === 'abl'; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: 'ACL Beginner Contest', + abbreviationName: 'ABL', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: false, + isShownRoundLabel: false, + roundLabelWidth: '', + tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-2', + isShownTaskIndex: true, + }; + } + + getContestRoundLabel(_contestId: string): string { + return ''; + } +} + +export class ACLProvider extends ContestTableProviderBase { + protected setFilterCondition(): (taskResult: TaskResult) => boolean { + return (taskResult: TaskResult) => { + if (classifyContest(taskResult.contest_id) !== this.contestType) { + return false; + } + return taskResult.contest_id === 'acl1'; + }; + } + + getMetadata(): ContestTableMetaData { + return { + title: 'ACL Contest 1', + abbreviationName: 'ACL', + }; + } + + getDisplayConfig(): ContestTableDisplayConfig { + return { + isShownHeader: false, + isShownRoundLabel: false, + roundLabelWidth: '', + tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-2', + isShownTaskIndex: true, + }; + } + + getContestRoundLabel(_contestId: string): string { + return ''; + } +} + const regexForJoiFirstQualRound = /^(joi)(\d{4})(yo1)(a|b|c)$/i; export class JOIFirstQualRoundProvider extends ContestTableProviderBase { @@ -1209,13 +1273,18 @@ export const prepareContestProviderPresets = () => { ), /** - * Single group for ACL Practice Contest + * Group for AtCoder Library Contests (ACL) + * Includes ACL Practice, ACL Beginner, and ACL Contest 1 */ - AclPractice: () => - new ContestTableProviderGroup(`AtCoder Library Practice Contest`, { - buttonLabel: 'ACL Practice', - ariaLabel: 'Filter ACL Practice Contest', - }).addProvider(new ACLPracticeProvider(ContestType.ACL_PRACTICE)), + Acl: () => + new ContestTableProviderGroup(`AtCoder Library Contests`, { + buttonLabel: 'ACL', + ariaLabel: 'Filter ACL Contests', + }).addProviders( + new ACLPracticeProvider(ContestType.ACL_PRACTICE), + new ACLBeginnerProvider(ContestType.ABC_LIKE), + new ACLProvider(ContestType.ARC_LIKE), + ), JOIFirstQualRound: () => new ContestTableProviderGroup(`JOI 一次予選`, { @@ -1251,7 +1320,7 @@ export const contestTableProviderGroups = { tessokuBook: prepareContestProviderPresets().TessokuBook(), mathAndAlgorithm: prepareContestProviderPresets().MathAndAlgorithm(), dps: prepareContestProviderPresets().dps(), // Dynamic Programming (DP) Contests - aclPractice: prepareContestProviderPresets().AclPractice(), + acl: prepareContestProviderPresets().Acl(), joiFirstQualRound: prepareContestProviderPresets().JOIFirstQualRound(), joiSecondQualAndSemiFinalRound: prepareContestProviderPresets().JOISecondQualAndSemiFinalRound(), }; diff --git a/src/test/lib/utils/contest_table_provider.test.ts b/src/test/lib/utils/contest_table_provider.test.ts index 5a681f56a..b84d0778a 100644 --- a/src/test/lib/utils/contest_table_provider.test.ts +++ b/src/test/lib/utils/contest_table_provider.test.ts @@ -16,6 +16,8 @@ import { AGC001OnwardsProvider, ABCLikeProvider, ACLPracticeProvider, + ACLBeginnerProvider, + ACLProvider, EDPCProvider, TDPCProvider, FPS24Provider, @@ -40,6 +42,8 @@ import { taskResultsForARC104OnwardsProvider, taskResultsForAGC001OnwardsProvider, taskResultsForACLPracticeProvider, + taskResultsForACLBeginnerProvider, + taskResultsForACLProvider, taskResultsForABCLikeProvider, } from './test_cases/contest_table_provider'; @@ -70,6 +74,10 @@ vi.mock('$lib/utils/contest', () => ({ return ContestType.MATH_AND_ALGORITHM; } else if (contestId === 'practice2') { return ContestType.ACL_PRACTICE; + } else if (contestId === 'abl') { + return ContestType.ABC_LIKE; + } else if (contestId === 'acl1') { + return ContestType.ARC_LIKE; } else if ( [ 'tenka1-2017-beginner', @@ -137,6 +145,8 @@ vi.mock('$lib/utils/contest', () => ({ } } else if (contestId === 'abl') { return 'ACL Beginner Contest'; + } else if (contestId === 'acl1') { + return 'ACL Contest 1'; } else if (contestId === 'tenka1-2017-beginner') { return 'Tenka1 2017 Beginner'; } else if (contestId === 'tenka1-2018-beginner') { @@ -2242,6 +2252,116 @@ describe('ContestTableProviderBase and implementations', () => { }); }); + describe('ACL Beginner Provider', () => { + test('filters tasks by contest_id (abl)', () => { + const provider = new ACLBeginnerProvider(ContestType.ABC_LIKE); + const filtered = provider.filter(taskResultsForACLBeginnerProvider); + + expect(filtered).toBeDefined(); + expect(filtered?.length).toBe(3); + expect(filtered?.every((task) => task.contest_id === 'abl')).toBe(true); + }); + + test('returns correct metadata', () => { + const provider = new ACLBeginnerProvider(ContestType.ABC_LIKE); + const metadata = provider.getMetadata(); + + expect(metadata.title).toBe('ACL Beginner Contest'); + expect(metadata.abbreviationName).toBe('ABL'); + }); + + test('returns correct display config', () => { + const provider = new ACLBeginnerProvider(ContestType.ABC_LIKE); + const config = provider.getDisplayConfig(); + + expect(config.isShownHeader).toBe(false); + expect(config.isShownRoundLabel).toBe(false); + expect(config.isShownTaskIndex).toBe(true); + expect(config.tableBodyCellsWidth).toBe( + 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-2', + ); + }); + + test('getContestRoundLabel returns empty string', () => { + const provider = new ACLBeginnerProvider(ContestType.ABC_LIKE); + const label = provider.getContestRoundLabel('abl_a'); + + expect(label).toBe(''); + }); + + test('generates correct table structure', () => { + const provider = new ACLBeginnerProvider(ContestType.ABC_LIKE); + const filtered = provider.filter(taskResultsForACLBeginnerProvider); + const table = provider.generateTable(filtered!); + + expect(table).toBeDefined(); + expect(table).not.toBeNull(); + }); + + test('does not include tasks with different contest_id', () => { + const provider = new ACLBeginnerProvider(ContestType.ABC_LIKE); + const allTasks = [...taskResultsForACLBeginnerProvider, ...taskResultsForACLProvider]; + const filtered = provider.filter(allTasks); + + expect(filtered?.every((task) => task.contest_id === 'abl')).toBe(true); + }); + }); + + describe('ACL Provider', () => { + test('filters tasks by contest_id (acl1)', () => { + const provider = new ACLProvider(ContestType.ARC_LIKE); + const filtered = provider.filter(taskResultsForACLProvider); + + expect(filtered).toBeDefined(); + expect(filtered?.length).toBe(6); + expect(filtered?.every((t) => t.contest_id === 'acl1')).toBe(true); + }); + + test('returns correct metadata', () => { + const provider = new ACLProvider(ContestType.ARC_LIKE); + const metadata = provider.getMetadata(); + + expect(metadata.title).toBe('ACL Contest 1'); + expect(metadata.abbreviationName).toBe('ACL'); + }); + + test('returns correct display config', () => { + const provider = new ACLProvider(ContestType.ARC_LIKE); + const config = provider.getDisplayConfig(); + + expect(config.isShownHeader).toBe(false); + expect(config.isShownRoundLabel).toBe(false); + expect(config.isShownTaskIndex).toBe(true); + expect(config.tableBodyCellsWidth).toBe( + 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-2', + ); + }); + + test('getContestRoundLabel returns empty string', () => { + const provider = new ACLProvider(ContestType.ARC_LIKE); + const label = provider.getContestRoundLabel('acl1_a'); + + expect(label).toBe(''); + }); + + test('generates correct table structure', () => { + const provider = new ACLProvider(ContestType.ARC_LIKE); + const filtered = provider.filter(taskResultsForACLProvider); + const table = provider.generateTable(filtered!); + + expect(table).toBeDefined(); + expect(table).not.toBeNull(); + }); + + test('does not include tasks with different contest_id', () => { + const provider = new ACLProvider(ContestType.ARC_LIKE); + const allTasks = [...taskResultsForACLBeginnerProvider, ...taskResultsForACLProvider]; + const filtered = provider.filter(allTasks); + + expect(filtered?.every((task) => task.contest_id === 'acl1')).toBe(true); + }); + }); + describe('JOI First Qual Round provider', () => { test('expects to filter tasks to include only JOI contests', () => { const provider = new JOIFirstQualRoundProvider(ContestType.JOI); diff --git a/src/test/lib/utils/test_cases/contest_table_provider.ts b/src/test/lib/utils/test_cases/contest_table_provider.ts index 63d90eb6d..5deab83e6 100644 --- a/src/test/lib/utils/test_cases/contest_table_provider.ts +++ b/src/test/lib/utils/test_cases/contest_table_provider.ts @@ -814,3 +814,24 @@ export const taskResultsForABCLikeProvider: TaskResults = [ jsc2025advance_final_a, jsc2025advance_final_h, ]; + +// ACL Contest 1: 6 tasks (A~F) +const [acl1_a, acl1_b, acl1_c, acl1_d, acl1_e, acl1_f] = createContestTasks('acl1', [ + { taskTableIndex: 'A', statusName: AC }, + { taskTableIndex: 'B', statusName: AC }, + { taskTableIndex: 'C', statusName: AC_WITH_EDITORIAL }, + { taskTableIndex: 'D', statusName: TRYING }, + { taskTableIndex: 'E', statusName: PENDING }, + { taskTableIndex: 'F', statusName: PENDING }, +]); + +export const taskResultsForACLBeginnerProvider: TaskResults = [abl_a, abl_b, abl_f]; + +export const taskResultsForACLProvider: TaskResults = [ + acl1_a, + acl1_b, + acl1_c, + acl1_d, + acl1_e, + acl1_f, +]; diff --git a/src/test/lib/utils/test_cases/contest_type.ts b/src/test/lib/utils/test_cases/contest_type.ts index 5a48f56be..3eefd3a77 100644 --- a/src/test/lib/utils/test_cases/contest_type.ts +++ b/src/test/lib/utils/test_cases/contest_type.ts @@ -305,6 +305,10 @@ export const arcLike = [ contestId: 'nikkei2019-qual', expected: ContestType.ARC_LIKE, }), + createTestCaseForContestType('ACL1')({ + contestId: 'acl1', + expected: ContestType.ARC_LIKE, + }), ]; export const agcLike = [ From b2acf238da502e3bce60bbe17e75b2b18e1e26a8 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Sun, 1 Feb 2026 10:10:46 +0000 Subject: [PATCH 2/2] refactor: Remove old plan (#3120) --- .../add-and-update-acl-providers/plan.md | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 docs/dev-notes/2026-02-01/add-and-update-acl-providers/plan.md diff --git a/docs/dev-notes/2026-02-01/add-and-update-acl-providers/plan.md b/docs/dev-notes/2026-02-01/add-and-update-acl-providers/plan.md deleted file mode 100644 index 82ce8a81d..000000000 --- a/docs/dev-notes/2026-02-01/add-and-update-acl-providers/plan.md +++ /dev/null @@ -1,91 +0,0 @@ -# 計画: ACLBeginnerProvider と ACLProvider を追加 (#3120) - -## 概要 - -既存の `ACLPracticeProvider` グループの下に `ACLBeginnerProvider` と `ACLProvider` を追加し、ACL Beginner Contest と ACL Contest 1 に対応させる。 - -## 変更内容 - -### 1. `src/lib/utils/contest_table_provider.ts` に Provider クラスを追加 - -**ACLPracticeProvider の位置**: 770行目以降に、以下の 2 つのクラスを順に追加: - -- `ACLBeginnerProvider` (ContestType.ABC_LIKE、contest_id: 'abl') -- `ACLProvider` (ContestType.ARC_LIKE、contest_id: 'acl1') - -参考: [how-to-add-contest-table-provider.md - パターン2](../../guides/how-to-add-contest-table-provider.md#パターン2-単一ソース型edpc--tdpc--fps_24--acl_practice) - -**主な設定**: - -- 両者とも contest_id による文字列比較でフィルタリング -- getDisplayConfig: `tableBodyCellsWidth: 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-2'` -- getContestRoundLabel: 空文字列を返す -- 両者とも taskIndex を表示(isShownTaskIndex: true) - -### 2. `prepareContestProviderPresets()` を更新(1214行目付近) - -**変更前**: - -``` -AclPractice: () => - new ContestTableProviderGroup(`AtCoder Library Practice Contest`, { - buttonLabel: 'ACL Practice', - ariaLabel: 'Filter ACL Practice Contest', - }).addProvider(new ACLPracticeProvider(ContestType.ACL_PRACTICE)), -``` - -**変更後**: `Acl` に名前変更し、3 つのプロバイダすべてを追加 - -``` -Acl: () => - new ContestTableProviderGroup(`AtCoder Library Contests`, { - buttonLabel: 'ACL', - ariaLabel: 'Filter ACL Contests', - }).addProviders( - new ACLPracticeProvider(ContestType.ACL_PRACTICE), - new ACLBeginnerProvider(ContestType.ABC_LIKE), - new ACLProvider(ContestType.ARC_LIKE), - ), -``` - -### 3. `contestTableProviderGroups` を更新(1250行目付近) - -**変更前**: - -``` -aclPractice: prepareContestProviderPresets().AclPractice(), -``` - -**変更後**: - -``` -acl: prepareContestProviderPresets().Acl(), -``` - -### 4. `src/test/lib/utils/test_cases/contest_table_provider.ts` にテストデータを追加 - -`createContestTasks()` を使用してモックデータを作成: - -- `taskResultsForACLBeginnerProvider`: contest_id 'abl' の 6 つのタスク(A~F) -- `taskResultsForACLProvider`: contest_id 'acl1' の 6 つのタスク(A~F) - -(prisma/tasks.ts の既存タスク定義を使用) - -### 5. `src/test/lib/utils/contest_table_provider.test.ts` にテストを追加 - -各新規プロバイダについて、以下の項目をカバーするテストを追加: - -- getMetadata() の検証(title、abbreviationName) -- getDisplayConfig() の検証 -- filter() の動作確認 -- generateTable() のテーブル構造 -- getContestRoundIds() と getHeaderIdsForTask() -- getContestRoundLabel() が空文字列を返すことを検証 - ---- - -## 参考資料 - -- 実装パターン: [how-to-add-contest-table-provider.md - パターン2](../../guides/how-to-add-contest-table-provider.md#パターン2-単一ソース型edpc--tdpc--fps_24--acl_practice) -- GitHub Issue: #3120 -- prisma/tasks.ts の既存データ(abl: 6 問、acl1: 6 問)