From daba317b77e4ffaae692bff5a4ba70e87090ceb4 Mon Sep 17 00:00:00 2001 From: max747 Date: Thu, 30 Apr 2026 00:28:27 +0900 Subject: [PATCH 1/3] =?UTF-8?q?sourceQuestIds=20=E3=82=92=20additionalSour?= =?UTF-8?q?ceQuestIds=20=E3=81=AB=E3=83=AA=E3=83=8D=E3=83=BC=E3=83=A0?= =?UTF-8?q?=E3=81=97=E3=80=81questId=20=E3=82=92=E5=B8=B8=E3=81=AB?= =?UTF-8?q?=E3=83=95=E3=82=A7=E3=83=83=E3=83=81=E5=AF=BE=E8=B1=A1=E3=81=AB?= =?UTF-8?q?=E5=90=AB=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 複数ソースを設定した際に questId 本体のデータがフェッチされないバグを修正。 aggregator は常に questId を先頭ソースとし、additionalSourceQuestIds を追加ソースとして扱う。 Co-Authored-By: Claude Sonnet 4.6 --- admin/src/pages/EventFormPage.tsx | 27 ++++++++++++++++----------- admin/src/types/index.ts | 2 +- lambda/aggregator/handler.py | 2 +- lambda/aggregator/test_handler.py | 26 +++++++++++++------------- viewer/src/types.ts | 2 +- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/admin/src/pages/EventFormPage.tsx b/admin/src/pages/EventFormPage.tsx index c9e2ed8..69f2215 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,20 @@ export function EventFormPage() { const addSourceId = (index: number) => { const id = (newSourceId[index] ?? "").trim(); if (!id) return; - const current = quests[index].sourceQuestIds ?? []; + 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 +118,10 @@ export function EventFormPage() { const addAsSource = (questIndex: number, sourceId: string) => { if (questIndex < 0 || questIndex >= quests.length) return; - const current = quests[questIndex].sourceQuestIds ?? []; + 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 +195,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 +473,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 +493,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 { From 0acce26e1132d173772a18b608c856c9da666b39 Mon Sep 17 00:00:00 2001 From: max747 Date: Thu, 30 Apr 2026 08:42:20 +0900 Subject: [PATCH 2/3] =?UTF-8?q?questId=20=E8=87=AA=E8=BA=AB=E3=82=92=20add?= =?UTF-8?q?itionalSourceQuestIds=20=E3=81=AB=E8=BF=BD=E5=8A=A0=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E5=88=B6=E9=99=90?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- admin/src/pages/EventFormPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/admin/src/pages/EventFormPage.tsx b/admin/src/pages/EventFormPage.tsx index 69f2215..a6373e4 100644 --- a/admin/src/pages/EventFormPage.tsx +++ b/admin/src/pages/EventFormPage.tsx @@ -97,6 +97,7 @@ export function EventFormPage() { const addSourceId = (index: number) => { const id = (newSourceId[index] ?? "").trim(); if (!id) return; + if (id === quests[index].questId) return; const current = quests[index].additionalSourceQuestIds ?? []; if (current.includes(id)) return; const updated = quests.map((q, i) => @@ -118,6 +119,7 @@ export function EventFormPage() { const addAsSource = (questIndex: number, sourceId: string) => { if (questIndex < 0 || questIndex >= quests.length) return; + if (sourceId === quests[questIndex].questId) return; const current = quests[questIndex].additionalSourceQuestIds ?? []; if (current.includes(sourceId)) return; const updated = quests.map((q, i) => From 952095cc97df7e978314bca3db4e59428b190202 Mon Sep 17 00:00:00 2001 From: max747 Date: Thu, 30 Apr 2026 08:42:22 +0900 Subject: [PATCH 3/3] =?UTF-8?q?SPEC.md=20=E3=81=AE=20sourceQuestIds=20?= =?UTF-8?q?=E3=82=92=20additionalSourceQuestIds=20=E3=81=AB=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- SPEC.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 同一報告者の複数報告