Skip to content

Commit 10b49c1

Browse files
committed
feat(ui): Split contest table for tessoku book (#2819)
1 parent 578d31b commit 10b49c1

File tree

4 files changed

+569
-48
lines changed

4 files changed

+569
-48
lines changed
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
````markdown
2+
# ContestTableProviderBase へのプロバイダーキー機能追加計画
3+
4+
**作成日**: 2025-11-12
5+
6+
**対象ファイル**:
7+
8+
- `src/lib/utils/contest_table_provider.ts`
9+
- `src/test/lib/utils/contest_table_provider.test.ts`
10+
11+
**優先度**: High
12+
13+
---
14+
15+
## 1. 概要
16+
17+
### 背景
18+
19+
現在の `ContestTableProviderGroup` では、`ContestType` をキーとして Provider を管理しているため、同じ `ContestType` で複数の異なるテーブル(例:Tessoku Book の例題・応用・力試し)を持つことができません。
20+
21+
### 目的
22+
23+
- 同じ `ContestType` で複数のセクション('examples', 'practicals', 'challenges')を区別できる設計に改善
24+
- Provider 自身に ID 管理責務を持たせ、関心の分離を徹底
25+
- 後方互換性を維持しつつ、拡張性を確保
26+
27+
### 実装方針
28+
29+
`static createProviderKey()` メソッドで キー生成ロジックを一元化し、Provider 自身が `getProviderKey()` で自分の識別子を返すようにする。
30+
31+
---
32+
33+
## 2. 仕様要件
34+
35+
| 項目 | 仕様 | 備考 |
36+
| ----------------------------- | ------------------------------------------------------------------------ | ---------------------------------- |
37+
| **キー型** | `type ProviderKey = \`${ContestType}\` \| \`${ContestType}::${string}\`` | TypeScript テンプレートリテラル型 |
38+
| **セクション識別子** | 'examples', 'practicals', 'challenges' | 現在は TessokuBook のみ |
39+
| **getProviderKey() アクセス** | protected | ContestTableProviderGroup 内部のみ |
40+
| **後方互換性** | getProvider(contestType) で section 未指定時は複合キーなしで取得 | 既存コード変更なし |
41+
42+
---
43+
44+
## 3. 変更対象
45+
46+
### 3.1 ContestTableProviderBase クラス
47+
48+
#### 追加メンバー
49+
50+
```typescript
51+
// クラスプロパティ
52+
protected readonly section?: string;
53+
54+
// static メソッド
55+
static createProviderKey(contestType: ContestType, section?: string): string {
56+
return section ? `${contestType}::${section}` : `${contestType}`;
57+
}
58+
59+
// インスタンスメソッド(protected)
60+
protected getProviderKey(): string {
61+
return ContestTableProviderBase.createProviderKey(this.contestType, this.section);
62+
}
63+
```
64+
65+
#### コンストラクタ修正
66+
67+
```typescript
68+
constructor(contestType: ContestType, section?: string) {
69+
this.contestType = contestType;
70+
this.section = section;
71+
}
72+
```
73+
74+
#### 制約
75+
76+
- `contestType` は `readonly` に変更
77+
- `section` は `readonly` に変更
78+
79+
---
80+
81+
### 3.2 Tessoku Book プロバイダー修正
82+
83+
#### TessokuBookForExamplesProvider
84+
85+
```typescript
86+
constructor(contestType: ContestType) {
87+
super(contestType, 'examples');
88+
}
89+
```
90+
91+
#### TessokuBookForPracticalsProvider
92+
93+
```typescript
94+
constructor(contestType: ContestType) {
95+
super(contestType, 'practicals');
96+
}
97+
```
98+
99+
#### TessokuBookForChallengesProvider
100+
101+
```typescript
102+
constructor(contestType: ContestType) {
103+
super(contestType, 'challenges');
104+
}
105+
```
106+
107+
---
108+
109+
### 3.3 ContestTableProviderGroup クラス
110+
111+
#### Map キー型を変更
112+
113+
```typescript
114+
private providers = new Map<string, ContestTableProviderBase>();
115+
```
116+
117+
#### addProvider() メソッド修正
118+
119+
```typescript
120+
addProvider(provider: ContestTableProviderBase): this {
121+
const key = provider['getProviderKey']();
122+
this.providers.set(key, provider);
123+
124+
return this;
125+
}
126+
```
127+
128+
**注**: `protected` メソッドへのアクセスのため、角括弧表記を使用
129+
130+
#### addProviders() メソッド修正
131+
132+
```typescript
133+
addProviders(...providers: ContestTableProviderBase[]): this {
134+
providers.forEach((provider) => {
135+
const key = provider['getProviderKey']();
136+
this.providers.set(key, provider);
137+
});
138+
return this;
139+
}
140+
```
141+
142+
#### getProvider() メソッド修正(後方互換性維持)
143+
144+
```typescript
145+
getProvider(
146+
contestType: ContestType,
147+
section?: string,
148+
): ContestTableProviderBase | undefined {
149+
const key = ContestTableProviderBase.createProviderKey(contestType, section);
150+
return this.providers.get(key);
151+
}
152+
```
153+
154+
---
155+
156+
### 3.4 prepareContestProviderPresets() 修正
157+
158+
#### TessokuBook プリセット
159+
160+
```typescript
161+
TessokuBook: () =>
162+
new ContestTableProviderGroup(`競技プログラミングの鉄則`, {
163+
buttonLabel: '競技プログラミングの鉄則',
164+
ariaLabel: 'Filter Tessoku Book',
165+
}).addProviders(
166+
new TessokuBookForExamplesProvider(ContestType.TESSOKU_BOOK),
167+
new TessokuBookForPracticalsProvider(ContestType.TESSOKU_BOOK),
168+
new TessokuBookForChallengesProvider(ContestType.TESSOKU_BOOK),
169+
),
170+
```
171+
172+
**変更点**: 引数形式から `ContestTableProviderBase` インスタンスへ直接変更
173+
174+
---
175+
176+
## 4. テスト計画
177+
178+
### 4.1 追加・修正するテスト
179+
180+
**参照**: `docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md`
181+
182+
#### TessokuBook 関連テスト
183+
184+
- ✅ TessokuBookForExamplesProvider の getProviderKey() = 'TESSOKU_BOOK::examples'
185+
- ✅ TessokuBookForPracticalsProvider の getProviderKey() = 'TESSOKU_BOOK::practicals'
186+
- ✅ TessokuBookForChallengesProvider の getProviderKey() = 'TESSOKU_BOOK::challenges'
187+
- ✅ 3 つの Provider を同時登録できるか検証
188+
189+
#### ContestTableProviderGroup 関連テスト
190+
191+
- ✅ addProvider() で Provider 自身の getProviderKey() を使用
192+
- ✅ addProviders() で複数 Provider の複合キーを別々に登録
193+
- ✅ getProvider(ContestType.TESSOKU_BOOK, 'examples') で正しく取得
194+
- ✅ getProvider(ContestType.TESSOKU_BOOK, 'practicals') で正しく取得
195+
- ✅ getProvider(ContestType.TESSOKU_BOOK, 'challenges') で正しく取得
196+
- ✅ getProvider(ContestType.TESSOKU_BOOK) で section 未指定時は complex key なしで検索(後方互換性)
197+
198+
### 4.2 テスト実行
199+
200+
```bash
201+
# 単体テスト実行
202+
pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts
203+
204+
# Lint チェック
205+
pnpm lint src/lib/utils/contest_table_provider.ts
206+
pnpm lint src/test/lib/utils/contest_table_provider.test.ts
207+
208+
# Format 確認
209+
pnpm format src/lib/utils/contest_table_provider.ts
210+
```
211+
212+
---
213+
214+
## 5. 実装手順(Todo リスト)
215+
216+
| # | タスク | 説明 | 依存 |
217+
| --- | -------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---- |
218+
| 1 | **型定義追加** | `ProviderKey` 型を定義(テンプレートリテラル型) | |
219+
| 2 | **ContestTableProviderBase 修正** | `section` プロパティ追加、`readonly` 指定、`static createProviderKey()` 追加、`getProviderKey()` 実装 | 1 |
220+
| 3 | **Tessoku Book Provider 修正** | 3 つの子クラスの constructor に section を追加 | 2 |
221+
| 4 | **ContestTableProviderGroup 修正** | Map キー型変更、addProvider/addProviders/getProvider メソッド修正 | 2 |
222+
| 5 | **prepareContestProviderPresets 修正** | TessokuBook プリセット の引数形式を変更 | 3, 4 |
223+
| 6 | **既存テスト確認** | 現在のテストが修正後も通るか検証 | 5 |
224+
| 7 | **新規テスト追加** | 複合キー関連の新テストを 6~8 個追加 | 6 |
225+
| 8 | **Lint & Format** | ESLint, Prettier で統一 | 7 |
226+
| 9 | **最終検証** | 全テスト実行、coverage 確認 | 8 |
227+
228+
---
229+
230+
## 6. 教訓・参照ドキュメント
231+
232+
- **テストパターン**: TessokuBook テスト の構造を参照(`docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md`)
233+
234+
---
235+
236+
## 7. 注意点
237+
238+
### 実装時の留意事項
239+
240+
1. **protected メソッドへのアクセス**
241+
- `getProviderKey()` は protected なため、Group 内では `provider['getProviderKey']()` で呼び出す
242+
- TypeScript の暗黙の型チェックを通すため、角括弧表記が必須
243+
244+
2. **後方互換性**
245+
- `getProvider(contestType)` で section 未指定時、複合キーなしで検索
246+
- 既存の `getProvider(ContestType.ABC)` などの呼び出しが動作継続
247+
248+
3. **Prettier フォーマット**
249+
- 実装後は必ず `pnpm format` を実行
250+
- インデントは Tab、printWidth は 100
251+
252+
---
253+
254+
## 8. 完了チェックリスト
255+
256+
- [ ] 型定義 `ProviderKey` 追加完了
257+
- [ ] ContestTableProviderBase クラス修正完了
258+
- [ ] Tessoku Book 3 プロバイダー修正完了
259+
- [ ] ContestTableProviderGroup 修正完了
260+
- [ ] prepareContestProviderPresets 修正完了
261+
- [ ] 既存テスト全て通過
262+
- [ ] 新規テスト 6~8 個追加完了
263+
- [ ] Lint エラーなし
264+
- [ ] Format 統一完了
265+
- [ ] 最終テスト実行 & coverage 確認完了
266+
267+
---
268+
269+
## 9. 実装完了報告
270+
271+
**実施日**: 2025-11-12
272+
273+
### 実施内容
274+
275+
すべてのタスク(1~9)を完了しました。
276+
277+
### 主な成果
278+
279+
1. **ProviderKey 型定義**: テンプレートリテラル型 `ProviderKey = \`${ContestType}\` | \`${ContestType}::${string}\`` を定義
280+
2. **ContestTableProviderBase 拡張**: `section` プロパティ、`static createProviderKey()`、`protected getProviderKey()` を追加
281+
3. **Tessoku Book プロバイダー強化**: 3 つの子クラスに section パラメータを追加('examples', 'practicals', 'challenges')
282+
4. **ContestTableProviderGroup 再設計**: Map キーを文字列に変更し、複合キーに対応
283+
5. **API 統一化**: `addProvider(provider)`, `addProviders(...providers)`, `getProvider(contestType, section?)` の新シグネチャに統一
284+
6. **後方互換性維持**: 既存コード(`getProvider(contestType)` のセクション省略)は引き続き動作
285+
286+
### テスト結果
287+
288+
- **全テスト実行結果**: 1,646 テスト合格(1 スキップ)
289+
- **contest_table_provider.test.ts**: 105 テスト合格
290+
- **新規テスト追加**: 複合キー機能に関する 7 個の新テスト追加
291+
292+
### コード品質
293+
294+
- **Prettier**: フォーマット完了(Tab インデント、printWidth 100)
295+
- **ESLint**: 警告なし(設定ファイル互換性警告のみ)
296+
- **型安全性**: TypeScript strict mode で検証済み
297+
298+
---
299+
300+
## 10. 教訓
301+
302+
### 実装パターン
303+
304+
1. **ProviderKey 型の活用**: `type ProviderKey = \`${ContestType}\` | \`${ContestType}::${string}\`` でテンプレートリテラル型を定義し、メソッドの戻り値型として使用することで型安全性を向上
305+
2. **保護メソッドの外部アクセス**: Protected メソッドへの外部アクセスは角括弧表記 `provider['getProviderKey']()` で実装
306+
3. **複合キー設計**: 単純キー(ContestType)と複合キー(ContestType + section)の併存は後方互換性を損なわず拡張性を確保
307+
4. **静的ファクトリメソッド**: `createProviderKey()` を static メソッドで共通化することで、キー生成ロジックの一元管理を実現
308+
309+
### テスト戦略
310+
311+
1. **複合キー検証**: 複数 section を持つ Provider の登録・取得を個別テストで検証
312+
2. **後方互換性テスト**: セクション省略時の動作も明示的にテスト
313+
3. **ProviderKey 型確認**: テストでは戻り値の型を runtime で検証(string 型、`::` 区切り文字など)
314+
4. **新規テスト構成**: 既存テスト 98 個 + 新規テスト 7 個 = 合計 105 テスト
315+
316+
### 開発効率
317+
318+
- **段階的実装**: 型定義 → ベースクラス → 子クラス → Group クラス → プリセット の順序で実装することで、依存関係を最小化
319+
- **型安全性と実用性のバランス**: TypeScript のテンプレートリテラル型で型安全性を確保しつつ、runtime では string として柔軟に操作
320+
321+
---
322+
323+
**状態**: ✅ 完了(ProviderKey 型を活用)
324+
````

src/lib/types/contest_table_provider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ContestType } from '$lib/types/contest';
12
import type { TaskResults, TaskResult } from '$lib/types/task';
23

34
/**
@@ -67,6 +68,13 @@ export interface ContestTableProvider {
6768
getContestRoundLabel(contestId: string): string;
6869
}
6970

71+
/**
72+
* Type for provider key
73+
* Supports simple contest type keys (e.g., 'ABC') and complex keys with sections
74+
* (e.g., 'TESSOKU_BOOK::examples', 'TESSOKU_BOOK::practicals', 'TESSOKU_BOOK::challenges')
75+
*/
76+
export type ProviderKey = `${ContestType}` | `${ContestType}::${string}`;
77+
7078
/**
7179
* Represents a two-dimensional table of contest results.
7280
*

0 commit comments

Comments
 (0)