/**
* GET /ds-alerts — D-Secアラート一覧+サマリ
*
* 形は `app/api/ds-devices/route.ts` と同じ。
* 仕様の正は `d-watch-api.yaml` の /ds-alerts。
*
* ★対応状況・備考の更新は `PATCH /errors/{error_id}/status`(`robot_error_reports` の同じ行。
* 指示書AX D6)。このルートは読むだけ★
*/
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { filterRowsByFacilityIds } from "@/lib/facilities/filter-by-ids";
import { resolveFacilityIdsByMasters } from "@/lib/facilities/resolve-by-master";
import {
filterDsAlertsByStatus,
filterDsAlertsByType,
parseDsAlertQuery,
sortDsAlerts,
summarizeDsAlerts,
} from "@/lib/ds-alerts/query";
import { withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type { ApiError, DsAlert, SummaryListResponse } from "@/lib/types";
import type { DsAlertSummary } from "@/lib/ds-alerts/query";
const ROUTE = "/api/ds-alerts";
function errorResponse(error: ApiError, status: number) {
return NextResponse.json(error, { status });
}
export async function GET(request: Request) {
return withRequestId(request, async () => {
const { searchParams } = new URL(request.url);
const parsed = parseDsAlertQuery(searchParams);
if (!parsed.ok) {
return errorResponse(parsed.error, parsed.status);
}
let rows: DsAlert[];
// ★分類(`group_id` / `customer_id`)→ 施設IDの配列★(指示書AK 段階4)。
// ★`null` = 絞らない / `[]` = 0件★(★罠1★。`?.length` で分岐しないこと)。
let facilityIds: number[] | null;
try {
// ★解決(施設マスタの読み取り)も同じ try に入れる★ — 失敗したら 500。
// ★黙って絞らずに返さないこと★(他社のアラートが混ざったまま画面に出る)。
facilityIds = await resolveFacilityIdsByMasters(parsed.query);
// ★全件(`summary.resolved` に解消も数える。指示書BB D17)★
rows = await db.dsAlerts({ open_only: false });
} catch (e) {
const body: ApiError = {
error: { code: "internal_error", message: "データの取得に失敗しました", details: null },
};
logApiError({ route: ROUTE, method: "GET", body, status: 500, err: e });
return errorResponse(body, 500);
}
// ★★分類の絞り込みは summary より**前**にかける★★(指示書AK 段階4)—
// type / status は「見え方」の絞り込みなので summary に効かせない(D-054)が、
// ★分類は「母集団」そのものを選ぶ★ので ★summary にも効かせる★
// (指示書AK §7-2 の数値カードと同じ理由)。
// ★★一覧と summary で母集団がずれないよう、必ず同じ配列から作ること★★。
// ★なぜ SQL ではなくここで絞るのかは `lib/facilities/filter-by-ids.ts` の冒頭★。
const scoped = filterRowsByFacilityIds(rows, facilityIds);
// ★summaryはtype/statusフィルタの前(全件)に対して計算する(D-054と同じ理由)★
// フィルタ後に計算すると、既定の絞り込み状態(status=open)によって
// サマリーが変わってしまう
const summary: DsAlertSummary = summarizeDsAlerts(scoped);
const filtered = filterDsAlertsByStatus(
filterDsAlertsByType(scoped, parsed.query.type),
parsed.query.status
);
const sorted = sortDsAlerts(filtered);
const { page, perPage } = parsed.query;
// ★meta.total と summary の合計は意味が違う★
// meta.total = 一覧(type・statusフィルタ後)の総件数。ページング計算用
// summary = 全アラート(type・statusの影響を受けない)に対する集計
// 絞り込みが無いとき(type=all かつ status=all)だけ一致する
const total = sorted.length;
const start = (page - 1) * perPage;
const items = sorted.slice(start, start + perPage);
const body: SummaryListResponse<DsAlert, DsAlertSummary> = {
summary,
items,
meta: { total, page, per_page: perPage },
};
return NextResponse.json(body);
});
}