Skip to content

Commit a2bf055

Browse files
authored
Merge pull request #2628 from AtCoder-NoviSteps/#2627
✨ Create ContestTaskPair table (#2627)
2 parents 28acb0e + 0c431f4 commit a2bf055

File tree

7 files changed

+702
-13
lines changed

7 files changed

+702
-13
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# Contest-Problem Mapping Extension Plan
2+
3+
## 概要
4+
5+
既存のTask テーブルのunique制約を維持しながら、複数コンテストで同一問題を扱うための拡張方針を検討。AtCoder Problems の実装を参考に、段階的な移行戦略を策定。
6+
7+
## 現状の課題
8+
9+
### 課題1: Task識別の制限
10+
11+
- **問題**: `task_id` が同じでも `contest_id` が異なるケースが多数存在
12+
- **制約**: Task テーブルの `task_id` unique制約は必ず維持する必要がある
13+
- **影響**: AtCoder Problems API の Problems API で登録済みの (task_id, contest_id) の問題のみ処理可能
14+
15+
### 課題2: 影響範囲の調査
16+
17+
- 数万行のTypeScript・Svelteコード
18+
- `map<string, TaskResult>` 形式の処理が広範囲に存在
19+
- e2eテストがほぼ実装されていないので、手動での確認が必要
20+
21+
## 解決方針
22+
23+
### 基本アプローチ
24+
25+
AtCoder Problems API を活用:
26+
27+
- 問題情報: 同一の `task_id` が存在する場合は、どれか一つのコンテストに割り当て
28+
- `https://kenkoooo.com/atcoder/resources/problems.json`
29+
- コンテストと問題idの対応関係を網羅
30+
- `https://kenkoooo.com/atcoder/resources/contest-problem.json`
31+
32+
### 技術的解決策
33+
34+
#### 1. 複合キーによる識別
35+
36+
**方針**: `map<pair<string, string>, TaskResult>` への移行
37+
38+
```typescript
39+
// 型定義
40+
type TaskKey = `${string}:${string}`; // "contest_id:task_id"
41+
42+
// ヘルパー関数
43+
function createTaskKey(contestId: string, taskId: string): TaskKey {
44+
return `${contestId}:${taskId}`;
45+
}
46+
47+
// マップの型
48+
type TaskResultMap = Map<TaskKey, TaskResult>;
49+
```
50+
51+
**利点**:
52+
53+
- 文字列連結は一般的で高速
54+
- TypeScriptのTuple型も利用可能だが、文字列の方が実用的
55+
- O(1)での参照が可能
56+
57+
#### 2. データマイグレーション戦略
58+
59+
```typescript
60+
// 既存データの移行
61+
function migrateExistingData(oldMap: Map<string, TaskResult>): Map<TaskKey, TaskResult> {
62+
const newMap = new Map<TaskKey, TaskResult>();
63+
64+
for (const [taskId, result] of oldMap) {
65+
const primaryContestId = result.contest_id || 'primary';
66+
const key = createTaskKey(primaryContestId, taskId);
67+
newMap.set(key, result);
68+
}
69+
70+
return newMap;
71+
}
72+
73+
// 互換性レイヤー
74+
export class TaskMapAdapter {
75+
constructor(
76+
private enhanced: EnhancedTaskMap,
77+
private legacy: LegacyTaskMap,
78+
) {}
79+
80+
get(taskId: string, contestId?: string): TaskResult | undefined {
81+
if (contestId) {
82+
return this.enhanced.get(createTaskKey(contestId, taskId));
83+
}
84+
return this.legacy.get(taskId);
85+
}
86+
}
87+
```
88+
89+
#### 3. インポート処理の分離
90+
91+
**方針**: 問題データインポートとマッピングデータインポートを別ページで処理
92+
93+
```typescript
94+
interface ImportOptions {
95+
includeContestMapping?: boolean; // contest-problem.json を使用
96+
onlyProblems?: boolean; // problems.json のみ使用
97+
}
98+
99+
// 問題データインポート(既存制約を守る)
100+
async function importTaskData(options: ImportOptions = {}) {
101+
if (options.onlyProblems !== false) {
102+
await importFromProblemsJson();
103+
}
104+
105+
if (options.includeContestMapping) {
106+
await importContestProblemMapping();
107+
}
108+
}
109+
```
110+
111+
**分離理由**:
112+
113+
- 責務の分離(異なるAPIエンドポイント)
114+
- エラーハンドリングの違い
115+
- 将来のコンテストサイト固有設定への対応
116+
117+
## 実装計画
118+
119+
### Phase 1: 基盤整備
120+
121+
- [ ] 新しい型定義の作成
122+
- [ ] ユーティリティ関数の実装
123+
- [ ] 互換性レイヤーの作成
124+
125+
### Phase 2: 影響範囲調査
126+
127+
```bash
128+
# 調査用コマンド
129+
grep -r "taskId" src/ --include="*.ts" --include="*.svelte"
130+
grep -r "Map<string," src/ --include="*.ts"
131+
grep -r "TaskResult" src/ --include="*.ts"
132+
```
133+
134+
### Phase 3: 段階的移行
135+
136+
1. 新しい型定義を導入
137+
2. 互換性レイヤーで既存コードをラップ
138+
3. モジュール単位で新しいAPIに移行
139+
4. テストで検証
140+
141+
### Phase 4: インポート機能拡張
142+
143+
- [ ] マッピングデータ用の管理画面作成
144+
- [ ] フィルタリング機能の実装
145+
- [ ] バリデーション機能の追加
146+
147+
## データ投入方法
148+
149+
### 推奨アプローチ: 管理者向けWebインターフェース
150+
151+
**実装コスト**: 14-21時間(目安)
152+
153+
```typescript
154+
// 基本機能
155+
interface MappingImportPage {
156+
importContestProblemMapping(filters: ImportFilter): Promise<void>;
157+
previewMappingChanges(): Promise<MappingPreview>;
158+
validateMappingData(): Promise<ValidationResult>;
159+
}
160+
161+
interface ImportFilter {
162+
contestIds?: string[];
163+
taskIds?: string[];
164+
dateRange?: { from: Date; to: Date };
165+
}
166+
```
167+
168+
**利点**:
169+
170+
- 長期的な保守性
171+
- エラーハンドリングの容易さ
172+
- 他コンテストサイト対応への拡張性
173+
174+
### ディレクトリ構成案
175+
176+
```
177+
src/routes/admin/import/
178+
├── problems/ # 問題データインポート
179+
│ ├── +page.svelte
180+
│ └── types.ts
181+
├── mapping/ # コンテスト-問題マッピング
182+
│ ├── +page.svelte
183+
│ ├── bulk/
184+
│ └── selective/
185+
└── shared/ # 共通コンポーネント
186+
├── ImportStatus.svelte
187+
└── ValidationResults.svelte
188+
```
189+
190+
## 今後の課題
191+
192+
### 短期
193+
194+
- [ ] 影響範囲の詳細調査
195+
- [ ] プロトタイプの作成
196+
- [ ] 基本的なe2eテストの追加
197+
198+
### 中期
199+
200+
- [ ] 他コンテストサイトAPI対応
201+
- [ ] APIレート制限への対応
202+
- [ ] UI/UXの改善
203+
204+
### 長期
205+
206+
- [ ] 完全自動化システム
207+
- [ ] 包括的なテストスイート
208+
- [ ] パフォーマンス最適化
209+
210+
## リスク要因と対策
211+
212+
| リスク | 影響度 | 対策 |
213+
| -------------------- | ------ | -------------------- |
214+
| 既存機能の破綻 || 互換性レイヤーの実装 |
215+
| データ不整合 || バリデーション強化 |
216+
| パフォーマンス劣化 || 段階的な最適化 |
217+
| メンテナンスコスト増 || ドキュメント整備 |
218+
219+
## 決定事項
220+
221+
1. **Task テーブルのunique制約は維持**
222+
2. **複合キー (`contest_id:task_id`) による識別**
223+
3. **段階的移行による安全な実装**
224+
4. **インポート機能の責務分離**
225+
5. **Web UI による管理画面実装**
226+
227+
## 参考リンク
228+
229+
- [API client in AtCoder Problems](https://github.com/kenkoooo/AtCoderProblems/blob/master/atcoder-problems-frontend/src/api/APIClient.ts#L239)
230+
- [Problems API](https://kenkoooo.com/atcoder/resources/problems.json)
231+
- [Contest-Problem API](https://kenkoooo.com/atcoder/resources/contest-problem.json)
232+
233+
---
234+
235+
**作成日**: 2025-09-17
236+
**最終更新**: 2025-09-17
237+
**ステータス**: 計画段階

0 commit comments

Comments
 (0)