Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)

Expand Down Expand Up @@ -391,22 +391,22 @@ report_id: `605fc0f1` — `items` にイベントアイテム (ぐん肥/のび

同一クエストのデータが Harvest 上で複数のページ ID に分割されている場合がある(例: イベント期間途中でページが分割されたケース)。

このような場合は `events.json` の `quests[].sourceQuestIds` に全ページ ID を列挙することで対応する:
このような場合は `events.json` の `quests[].additionalSourceQuestIds` に追加ページ ID を列挙することで対応する:

```json
{
"questId": "XCtBEoEwgr6R",
"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 同一報告者の複数報告

Expand Down
29 changes: 18 additions & 11 deletions admin/src/pages/EventFormPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] },
]);
Comment thread
max747 marked this conversation as resolved.
};

const toggleSourceExpanded = (index: number) => {
Expand All @@ -94,31 +97,33 @@ 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);
Comment thread
max747 marked this conversation as resolved.
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);
};

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 }));
Expand Down Expand Up @@ -192,7 +197,9 @@ export function EventFormPage() {

if (loading) return <p>読み込み中...</p>;

const addedIds = new Set(quests.flatMap((q) => [q.questId, ...(q.sourceQuestIds ?? [])]));
const addedIds = new Set(
quests.flatMap((q) => [q.questId, ...(q.additionalSourceQuestIds ?? [])]),
);

return (
<div style={{ maxWidth: 800, margin: "24px auto", padding: 24 }}>
Expand Down Expand Up @@ -468,9 +475,9 @@ export function EventFormPage() {
}}
>
{sourceExpanded[i] ? "▲ 複数ソース設定を閉じる" : "▼ 複数ソース設定"}
{q.sourceQuestIds && q.sourceQuestIds.length > 0 && (
{q.additionalSourceQuestIds && q.additionalSourceQuestIds.length > 0 && (
<span style={{ marginLeft: 6, color: "#0066cc" }}>
({q.sourceQuestIds.length} 件設定中)
({q.additionalSourceQuestIds.length} 件設定中)
</span>
)}
</button>
Expand All @@ -488,7 +495,7 @@ export function EventFormPage() {
集計元の Harvest ページ ID を列挙します。未設定の場合はクエスト ID
のみが使われます。
</p>
{(q.sourceQuestIds ?? []).map((sid, si) => (
{(q.additionalSourceQuestIds ?? []).map((sid, si) => (
<div
key={si}
style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 4 }}
Expand Down
2 changes: 1 addition & 1 deletion admin/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export interface Quest {
name: string;
level: string;
ap: number;
sourceQuestIds?: string[];
additionalSourceQuestIds?: string[];
}

export interface EventPeriod {
Expand Down
2 changes: 1 addition & 1 deletion lambda/aggregator/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def fetch_harvest_reports(quest_id: str) -> 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)
Comment thread
max747 marked this conversation as resolved.

all_reports: list[dict] = []
Expand Down
26 changes: 13 additions & 13 deletions lambda/aggregator/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 に渡された引数を返す。"""
Expand All @@ -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"],
}
Comment thread
max747 marked this conversation as resolved.
reports_a = [_make_harvest_report("r1", {"素材A": "5"})]
reports_b = [_make_harvest_report("r2", {"素材A": "3"})]
Expand All @@ -218,22 +218,22 @@ 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
output = self._run(quest, [reports_a, reports_b])
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])
Expand All @@ -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"})]
Expand Down
2 changes: 1 addition & 1 deletion viewer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export interface Quest {
name: string;
level: string;
ap: number;
sourceQuestIds?: string[];
additionalSourceQuestIds?: string[];
}

export interface EventPeriod {
Expand Down
Loading