diff --git a/SPEC.md b/SPEC.md
index 21f1ba4..43762cc 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -102,7 +102,7 @@ FGO イベント報告データを集計し、アイテムドロップ率を算
| `quests[].name` | string | クエスト名 |
| `quests[].level` | string | 推奨レベル |
| `quests[].ap` | number | 消費 AP |
-| `quests[].sourceQuestIds` | string[] (省略可) | 集計元の Harvest ページ ID リスト。複数ページに分割されているクエストを統合する際に指定する。省略または空配列の場合は `questId` のみを使用する |
+| `quests[].additionalSourceQuestIds` | string[] (省略可) | `questId` 以外の追加集計元 Harvest ページ ID リスト。複数ページに分割されているクエストを統合する際に指定する。`questId` は常に集計元に含まれるため、このリストには含めない。省略または空配列の場合は `questId` のみを使用する |
### 3.2 除外リスト (`exclusions.json`)
@@ -391,7 +391,7 @@ report_id: `605fc0f1` — `items` にイベントアイテム (ぐん肥/のび
同一クエストのデータが Harvest 上で複数のページ ID に分割されている場合がある(例: イベント期間途中でページが分割されたケース)。
-このような場合は `events.json` の `quests[].sourceQuestIds` に全ページ ID を列挙することで対応する:
+このような場合は `events.json` の `quests[].additionalSourceQuestIds` に追加ページ ID を列挙することで対応する:
```json
{
@@ -399,14 +399,14 @@ report_id: `605fc0f1` — `items` にイベントアイテム (ぐん肥/のび
"name": "...",
"level": "90+",
"ap": 40,
- "sourceQuestIds": ["XCtBEoEwgr6R", "Ab12CdEfGhIj"]
+ "additionalSourceQuestIds": ["Ab12CdEfGhIj"]
}
```
-- 集計 Lambda は `sourceQuestIds` の各 ID から報告を取得し、重複排除後にマージして1つの中間 JSON を生成する
+- 集計 Lambda は常に `questId` を先頭ソースとし、`additionalSourceQuestIds` の各 ID を追加して報告を取得し、重複排除後にマージして1つの中間 JSON を生成する
- 出力ファイルパスは `questId` を使用 (`{eventId}/{questId}.json`)
- 公開画面のルーティングや除外リストの管理は変わらない
-- `sourceQuestIds` 省略または空配列の場合は `questId` のみを使用する(後方互換)
+- `additionalSourceQuestIds` 省略または空配列の場合は `questId` のみを使用する
### 7.8 同一報告者の複数報告
diff --git a/admin/src/pages/EventFormPage.tsx b/admin/src/pages/EventFormPage.tsx
index c9e2ed8..a6373e4 100644
--- a/admin/src/pages/EventFormPage.tsx
+++ b/admin/src/pages/EventFormPage.tsx
@@ -84,7 +84,10 @@ export function EventFormPage() {
};
const addQuest = () => {
- setQuests([...quests, { questId: "", name: "", level: "", ap: 40, sourceQuestIds: [] }]);
+ setQuests([
+ ...quests,
+ { questId: "", name: "", level: "", ap: 40, additionalSourceQuestIds: [] },
+ ]);
};
const toggleSourceExpanded = (index: number) => {
@@ -94,20 +97,21 @@ export function EventFormPage() {
const addSourceId = (index: number) => {
const id = (newSourceId[index] ?? "").trim();
if (!id) return;
- const current = quests[index].sourceQuestIds ?? [];
+ if (id === quests[index].questId) return;
+ const current = quests[index].additionalSourceQuestIds ?? [];
if (current.includes(id)) return;
const updated = quests.map((q, i) =>
- i === index ? { ...q, sourceQuestIds: [...current, id] } : q,
+ i === index ? { ...q, additionalSourceQuestIds: [...current, id] } : q,
);
setQuests(updated);
setNewSourceId((prev) => ({ ...prev, [index]: "" }));
};
const removeSourceId = (questIndex: number, sourceIndex: number) => {
- const current = quests[questIndex].sourceQuestIds ?? [];
+ const current = quests[questIndex].additionalSourceQuestIds ?? [];
const updated = quests.map((q, i) =>
i === questIndex
- ? { ...q, sourceQuestIds: current.filter((_, si) => si !== sourceIndex) }
+ ? { ...q, additionalSourceQuestIds: current.filter((_, si) => si !== sourceIndex) }
: q,
);
setQuests(updated);
@@ -115,10 +119,11 @@ export function EventFormPage() {
const addAsSource = (questIndex: number, sourceId: string) => {
if (questIndex < 0 || questIndex >= quests.length) return;
- const current = quests[questIndex].sourceQuestIds ?? [];
+ if (sourceId === quests[questIndex].questId) return;
+ const current = quests[questIndex].additionalSourceQuestIds ?? [];
if (current.includes(sourceId)) return;
const updated = quests.map((q, i) =>
- i === questIndex ? { ...q, sourceQuestIds: [...current, sourceId] } : q,
+ i === questIndex ? { ...q, additionalSourceQuestIds: [...current, sourceId] } : q,
);
setQuests(updated);
setAddAsSourceTarget((prev) => ({ ...prev, [sourceId]: -1 }));
@@ -192,7 +197,9 @@ export function EventFormPage() {
if (loading) return
読み込み中...
;
- const addedIds = new Set(quests.flatMap((q) => [q.questId, ...(q.sourceQuestIds ?? [])]));
+ const addedIds = new Set(
+ quests.flatMap((q) => [q.questId, ...(q.additionalSourceQuestIds ?? [])]),
+ );
return (
@@ -468,9 +475,9 @@ export function EventFormPage() {
}}
>
{sourceExpanded[i] ? "▲ 複数ソース設定を閉じる" : "▼ 複数ソース設定"}
- {q.sourceQuestIds && q.sourceQuestIds.length > 0 && (
+ {q.additionalSourceQuestIds && q.additionalSourceQuestIds.length > 0 && (
- ({q.sourceQuestIds.length} 件設定中)
+ ({q.additionalSourceQuestIds.length} 件設定中)
)}
@@ -488,7 +495,7 @@ export function EventFormPage() {
集計元の Harvest ページ ID を列挙します。未設定の場合はクエスト ID
のみが使われます。
- {(q.sourceQuestIds ?? []).map((sid, si) => (
+ {(q.additionalSourceQuestIds ?? []).map((sid, si) => (
list[dict]:
def process_quest(event_id: str, quest: dict, event_items: set[str]) -> None:
"""クエスト1件を処理: 取得・変換・中間 JSON 出力。"""
quest_id = quest["questId"]
- source_ids = quest.get("sourceQuestIds") or [quest_id]
+ source_ids = [quest_id] + list(quest.get("additionalSourceQuestIds") or [])
logger.info("Processing quest %s (%s), sources: %s", quest_id, quest["name"], source_ids)
all_reports: list[dict] = []
diff --git a/lambda/aggregator/test_handler.py b/lambda/aggregator/test_handler.py
index 21d4b1d..141d790 100644
--- a/lambda/aggregator/test_handler.py
+++ b/lambda/aggregator/test_handler.py
@@ -159,7 +159,7 @@ def test_nan_becomes_none(self):
assert result["素材A"] is None
-# --- process_quest (sourceQuestIds) ---
+# --- process_quest (additionalSourceQuestIds) ---
def _make_harvest_report(rid: str, items: dict[str, str]) -> dict:
@@ -174,8 +174,8 @@ def _make_harvest_report(rid: str, items: dict[str, str]) -> dict:
}
-class TestProcessQuestSourceQuestIds:
- """process_quest の sourceQuestIds 対応"""
+class TestProcessQuestAdditionalSourceQuestIds:
+ """process_quest の additionalSourceQuestIds 対応"""
def _run(self, quest: dict, fetch_side_effect: list[list]) -> dict:
"""process_quest を実行し、write_json に渡された引数を返す。"""
@@ -187,22 +187,22 @@ def _run(self, quest: dict, fetch_side_effect: list[list]) -> dict:
_key, output = mock_write.call_args[0]
return output
- def test_single_source_backward_compat(self):
- """sourceQuestIds 未設定 → questId のみ取得"""
+ def test_single_source_no_additional(self):
+ """additionalSourceQuestIds 未設定 → questId のみ取得"""
quest = {"questId": "AAA", "name": "Q1", "level": "90+", "ap": 40}
reports_a = [_make_harvest_report("r1", {"素材A": "5"})]
output = self._run(quest, [reports_a])
assert len(output["reports"]) == 1
assert output["reports"][0]["id"] == "r1"
- def test_multiple_sources_merged(self):
- """sourceQuestIds 設定 → 全ソースのレポートがマージされる"""
+ def test_additional_sources_merged(self):
+ """additionalSourceQuestIds 設定 → questId + 追加ソースのレポートがマージされる"""
quest = {
"questId": "AAA",
"name": "Q1",
"level": "90+",
"ap": 40,
- "sourceQuestIds": ["AAA", "BBB"],
+ "additionalSourceQuestIds": ["BBB"],
}
reports_a = [_make_harvest_report("r1", {"素材A": "5"})]
reports_b = [_make_harvest_report("r2", {"素材A": "3"})]
@@ -218,7 +218,7 @@ def test_duplicate_report_ids_deduplicated(self):
"name": "Q1",
"level": "90+",
"ap": 40,
- "sourceQuestIds": ["AAA", "BBB"],
+ "additionalSourceQuestIds": ["BBB"],
}
reports_a = [_make_harvest_report("r1", {"素材A": "5"})]
reports_b = [_make_harvest_report("r1", {"素材A": "5"})] # 同じ ID
@@ -226,14 +226,14 @@ def test_duplicate_report_ids_deduplicated(self):
assert len(output["reports"]) == 1
assert output["reports"][0]["id"] == "r1"
- def test_empty_source_quest_ids_fallback(self):
- """sourceQuestIds が空リストの場合 questId にフォールバック"""
+ def test_empty_additional_source_quest_ids(self):
+ """additionalSourceQuestIds が空リストの場合 questId のみ取得"""
quest = {
"questId": "AAA",
"name": "Q1",
"level": "90+",
"ap": 40,
- "sourceQuestIds": [],
+ "additionalSourceQuestIds": [],
}
reports_a = [_make_harvest_report("r1", {"素材A": "5"})]
output = self._run(quest, [reports_a])
@@ -246,7 +246,7 @@ def test_output_key_uses_quest_id(self):
"name": "Q1",
"level": "90+",
"ap": 40,
- "sourceQuestIds": ["AAA", "BBB"],
+ "additionalSourceQuestIds": ["BBB"],
}
reports_a = [_make_harvest_report("r1", {"素材A": "5"})]
reports_b = [_make_harvest_report("r2", {"素材A": "3"})]
diff --git a/viewer/src/types.ts b/viewer/src/types.ts
index ae7d107..83a0f91 100644
--- a/viewer/src/types.ts
+++ b/viewer/src/types.ts
@@ -3,7 +3,7 @@ export interface Quest {
name: string;
level: string;
ap: number;
- sourceQuestIds?: string[];
+ additionalSourceQuestIds?: string[];
}
export interface EventPeriod {