Newer
Older
client_dwatch / app / api / ds-alerts / route.ts
/**
 * 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);
  });
}