仕様ドキュメント

設計書

03-design_ja.md

設計書 — 反社会的勢力チェックアプリケーション

1. アーキテクチャ概要

graph TB
  subgraph Client["クライアント (ブラウザ)"]
    UI["Next.js App Router<br/>React Server Components + Client Islands"]
  end

  UI -->|HTTPS| Backend

  subgraph Backend["Next.js バックエンド (Bun)"]
    direction TB
    subgraph Handlers[" "]
      direction LR
      API["API Routes<br/>(REST)"]
      SA["Server<br/>Actions"]
      BG["バックグラウンドジョブ<br/>(Bun worker threads)"]
    end

    subgraph Services["サービス層"]
      direction LR
      CRS["CheckRequestService"]
      SS["ScreeningService"]
      NS["NotificationSvc"]
    end

    subgraph Infra[" "]
      direction LR
      Prisma["Prisma ORM"]
      subgraph Clients["外部 API クライアント"]
        HB["HoujinBangouClient"]
        ED["EdinetClient"]
        KP["KanpouClient"]
        FSA["FsaClient"]
        MLIT["MlitClient"]
        GS["GoogleSearchClient"]
        NT["NikkeiTelecomHelper"]
      end
    end

    Handlers --> Services
    Services --> Infra
  end

  Prisma --> DB[("PostgreSQL")]
  NS --> SMTP["Email / SMTP"]

設計原則

  • SOLID: 各外部 API クライアントは単一責任で注入可能
  • DI: 外部依存はすべて注入 — テスト時にモック可能
  • SSOT: 法人番号をエンティティ同定の唯一の真実とする
  • KISS: 過剰抽象化は避け、3 層のスクリーニングを段階的に構築

2. プロジェクト構成

anti-social-check/
├── src/
│   ├── app/                          # Next.js App Router
│   │   ├── layout.tsx                # ルートレイアウト (日本語ロケール)
│   │   ├── page.tsx                  # ダッシュボード
│   │   ├── checks/
│   │   │   ├── page.tsx              # チェック依頼一覧
│   │   │   ├── new/page.tsx          # 新規依頼作成
│   │   │   ├── [id]/
│   │   │   │   ├── page.tsx          # 依頼詳細
│   │   │   │   ├── screening/page.tsx # スクリーニング実行画面
│   │   │   │   └── results/page.tsx  # 結果・判定
│   │   │   └── import/page.tsx       # CSV 一括取込
│   │   ├── ledger/
│   │   │   └── page.tsx              # 検索結果一覧表ビュー
│   │   ├── monitoring/
│   │   │   └── page.tsx              # 年次再チェックダッシュボード
│   │   ├── admin/
│   │   │   ├── keywords/page.tsx     # ネガティブキーワード管理
│   │   │   └── users/page.tsx        # ユーザー・ロール管理
│   │   └── api/
│   │       ├── checks/route.ts       # チェック CRUD
│   │       ├── screening/route.ts    # スクリーニング起動
│   │       ├── houjin/route.ts       # 法人番号参照プロキシ
│   │       ├── edinet/route.ts       # EDINET プロキシ
│   │       ├── osint/route.ts        # OSINT チェックプロキシ
│   │       └── export/route.ts       # Excel/PDF エクスポート
│   ├── services/                     # サービス層・外部クライアント
│   │   ├── check-request.service.ts
│   │   ├── screening.service.ts      # 全スクリーニング層を統括
│   │   ├── notification.service.ts
│   │   ├── export.service.ts
│   │   ├── houjin-bangou.ts          # 国税庁 法人番号 API
│   │   ├── edinet.ts                 # EDINET API
│   │   ├── kanpou.ts                 # 官報検索
│   │   ├── fsa.ts                    # 金融庁行政処分
│   │   ├── mlit.ts                   # 国土交通省ネガティブ情報
│   │   ├── google-search.ts          # Google カスタム検索 URL ビルダー
│   │   ├── nikkei-telecom.ts         # クエリビルダー (API 無し)
│   │   └── types.ts                  # 共通インターフェース
│   ├── db/
│   │   ├── schema.prisma
│   │   └── seed.ts
│   ├── types/
│   └── components/
├── prisma/
├── tests/
└── ...

3. データモデル

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// ─── ユーザー・ロール ──────────────────────────────────────

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  role      Role
  createdAt DateTime @default(now())

  createdChecks  CheckRequest[] @relation("CreatedBy")
  assignedChecks CheckRequest[] @relation("AssignedTo")
  auditLogs      AuditLog[]
}

enum Role {
  SALES           // 営業担当 — 依頼作成
  LEGAL           // グループ法務担当 — 検索実施
  FF_LEGAL        // FF 法務担当 — ヒット時の審査
  COMPLIANCE      // コンプライアンス責任者 — 最終承認
  ADMIN           // システム管理者
}

// ─── チェック依頼 ──────────────────────────────────────────

model CheckRequest {
  id              String       @id @default(cuid())
  trackingNumber  String       @unique // 例: "ASC-2026-0001"
  status          CheckStatus  @default(DRAFT)

  companyName     String
  companyAddress  String
  representativeName String
  companyType     CompanyType
  officerInfoDisclosed Boolean @default(false)

  corporateNumber String?      // 13 桁法人番号 (個人事業主は null)

  determination   Determination?
  determinationComment String?
  determinedAt    DateTime?

  entities        CheckEntity[]
  attachments     Attachment[]
  auditLogs       AuditLog[]

  createdById     String
  createdBy       User         @relation("CreatedBy", fields: [createdById], references: [id])
  assignedToId    String?
  assignedTo      User?        @relation("AssignedTo", fields: [assignedToId], references: [id])

  createdAt       DateTime     @default(now())
  updatedAt       DateTime     @updatedAt
  nextCheckDue    DateTime?    // 年次再チェック期限
  archivedAt      DateTime?

  @@index([status])
  @@index([corporateNumber])
  @@index([nextCheckDue])
}

enum CheckStatus {
  DRAFT
  SUBMITTED
  IN_REVIEW          // 法務が検索実施中
  AWAITING_FF_LEGAL  // エスカレーション中 — ヒットあり
  COMPLETED
  ARCHIVED
}

enum CompanyType {
  LISTED             // 上場
  UNLISTED_DISCLOSED // 未上場・役員開示あり
  UNLISTED_UNDISCLOSED // 未上場・役員開示なし
  SOLE_PROPRIETOR    // 個人事業主
}

enum Determination {
  NO_CONCERNS        // 取引開始に懸念ない
  TRANSACTION_REFUSED // 取引謝絶
}

// ─── チェック対象エンティティ ──────────────────────────────

model CheckEntity {
  id              String       @id @default(cuid())
  checkRequestId  String
  checkRequest    CheckRequest @relation(fields: [checkRequestId], references: [id], onDelete: Cascade)

  entityType      EntityType
  name            String       // 個人名または法人名
  nameKana        String?      // 同名判別用のカナ

  screeningResults ScreeningResult[]

  createdAt       DateTime     @default(now())

  @@index([checkRequestId])
}

enum EntityType {
  COMPANY          // 法人
  REPRESENTATIVE   // 代表者
  OFFICER          // 役員
  SHAREHOLDER      // 主要株主
}

// ─── スクリーニング結果 (エンティティ × ソース) ────────────

model ScreeningResult {
  id              String          @id @default(cuid())
  entityId        String
  entity          CheckEntity     @relation(fields: [entityId], references: [id], onDelete: Cascade)

  source          ScreeningSource
  status          ScreeningStatus @default(PENDING)

  hitCount        Int?
  summary         String?
  rawResponse     Json?           // API メタデータ (著作権保護対象の本文は保存しない)
  searchQuery     String?

  // 日経テレコンのエスカレーション用
  escalatedToFfLegal Boolean     @default(false)
  ffLegalReview      String?
  ffLegalReviewerId  String?
  ffLegalReviewedAt  DateTime?

  executedAt      DateTime?
  createdAt       DateTime     @default(now())

  @@unique([entityId, source])
  @@index([status])
}

enum ScreeningSource {
  HOUJIN_BANGOU     // 法人番号 API
  EDINET            // EDINET 開示
  KANPOU            // 官報
  FSA               // 金融庁
  MLIT              // 国土交通省
  NIKKEI_TELECOM    // 日経テレコン
  INTERNET_SEARCH   // Google 検索
}

enum ScreeningStatus {
  PENDING
  IN_PROGRESS
  CLEAR             // ヒット無し
  HIT               // ヒットあり — 要確認
  ESCALATED         // FF 法務へ送付
  REVIEWED          // FF 法務レビュー済み
  SKIPPED           // 当該エンティティには該当なし
}

// 以下 NegativeKeyword / Attachment / AuditLog は割愛 (EN 版と同一)

4. スクリーニングパイプライン設計

ScreeningService が多層チェックのフローを統括する。

flowchart TD
  Start(["ScreeningService.execute()<br/>入力: 解決済みエンティティ付き CheckRequest"])

  Start --> T1

  subgraph T1["Tier 1: OSINT (自動・並列)"]
    direction LR
    HB["HoujinBangou<br/>(名寄せ)"]
    KP["Kanpou"]
    FSA["FSA"]
    MLIT["MLIT"]
  end

  T1 --> HitCheck{"いずれかヒット?"}
  HitCheck -->|Yes| AutoFlag["自動フラグ付与"]
  HitCheck -->|No| T15
  AutoFlag --> T15

  subgraph T15["Tier 1.5: EDINET (上場企業のみ)"]
    EDINET["役員名取得 → CheckEntity 追加生成"]
  end

  T15 --> T2

  subgraph T2["Tier 2: 日経テレコン + インターネット検索 (半手動)"]
    direction TB
    GenNikkei["1. 日経テレコンクエリ生成"]
    GenGoogle["2. Google 検索 URL 生成"]
    UserSearch["3. 担当者が検索実行、件数入力"]
    Escalate{"日経ヒット > 0?"}
    EscFF["FF 法務へ自動エスカレーション"]

    GenNikkei --> GenGoogle --> UserSearch --> Escalate
    Escalate -->|Yes| EscFF
    Escalate -->|No| Done2[続行]
  end

  T2 --> Det

  subgraph Det["判定"]
    Review["全エンティティ検索完了 → 法務が判定記録"]
    Refused{"謝絶?"}
    Approve["コンプライアンス責任者承認必須"]

    Review --> Refused
    Refused -->|Yes| Approve
    Refused -->|No| Complete
  end

  Complete(["チェック完了"])
  Approve --> Complete

Tier 1: 自動 OSINT チェック

各クライアントは共通インターフェースを実装する:

export type SearchEntity = {
  name: string;
  address?: string;
  corporateNumber?: string;
};

export type ScreeningOutcome = {
  source: ScreeningSource;
  status: "clear" | "hit" | "error" | "manual_required";
  hitCount: number;
  summary: string | null;
  details: Record<string, unknown>;
  executedAt: Date;
};

export interface ScreeningClient {
  readonly source: ScreeningSource;
  search(entity: SearchEntity): Promise<ScreeningOutcome>;
}

Tier 1 の全チェックは各エンティティに対して Promise.all() で並列実行する。

Tier 2: 日経テレコン (クエリ生成のみ)

日経テレコンは公開 API を持たないため、アプリはクエリ文字列を生成するのみで Web UI の自動化は行わない。

class NikkeiTelecomHelper {
  buildSearchQuery(entityName: string, keywords: NegativeKeyword[]): string {
    const keywordClause = keywords
      .filter(k => k.isActive)
      .map(k => k.word)
      .join(" or ");
    return `${entityName} and (${keywordClause})`;
  }

  buildNarrowDownQuery(entityName: string): string {
    return entityName; // 「絞込み」用
  }

  getLoginUrl(): string {
    return "https://t21.nikkei.co.jp/";
  }
}

スクリーニング画面での業務フロー:

  1. システムが生成済みクエリと「日経テレコンを開く」ボタンを表示
  2. 担当者がログインして記事検索へ遷移、保存済み検索条件を実行
  3. 「絞込み」にシステム提示のエンティティ名を入力
  4. ヒット件数をアプリに入力
  5. 0 件 → clear、1 件以上 → FF 法務エスカレーション

Tier 2: インターネット検索 (URL 生成)

class GoogleSearchClient {
  buildSearchUrl(entityName: string, keywords: NegativeKeyword[]): string {
    const keywordClause = keywords
      .filter(k => k.isActive)
      .map(k => k.word)
      .join(" OR ");
    const query = `"${entityName}" (${keywordClause})`;
    return `https://www.google.com/search?q=${encodeURIComponent(query)}`;
  }
}

5. 外部 API 統合詳細(入出力仕様)

各クライアントは ScreeningClient インターフェースを実装し、統一された SearchEntity を入力、ScreeningOutcome を出力する。以下では共通形式に加え、クライアント固有の拡張メソッド・データ構造を示す。

5.1 国税庁 — 法人番号 Web-API

  • エンドポイント: https://api.houjin-bangou.nta.go.jp/4/{name|num}
  • 認証: アプリケーション ID (無償登録)
  • 用途: 法人名 + 住所から 13 桁法人番号を解決し、エンティティ同定の SSOT とする
  • レスポンス形式: XML (UTF-8, type=12)
  • レート制限: 1 req/sec の礼儀的な間隔

入力

メソッド パラメータ 説明
search(entity) SearchEntity { name, address? } ScreeningClient 実装。内部で lookupByName を呼ぶ
lookupByName(name, address?) name: string, address?: string 商号部分一致 (mode=2, target=1)。住所が与えられれば県+市+番地に含むもので絞込み
lookupByNumber(corporateNumber) corporateNumber: string (13 桁) 最新履歴のみ (history=0)

リクエストパラメータ (生):

id={appId}&name={name}&type=12&mode=2&target=1
id={appId}&number={13桁}&type=12&history=0

出力

共通 ScreeningOutcome に加え、details.corporations として以下を返す:

type CorporateInfo = {
  corporateNumber: string;   // 13 桁
  name: string;              // 商号
  furigana: string | null;   // フリガナ
  kind: string;              // 法人種別コード (株式会社=301 等)
  prefectureName: string;
  cityName: string;
  streetNumber: string;
  postCode: string;          // 郵便番号 (7 桁)
  assignmentDate: string;    // 法人番号指定年月日 (YYYY-MM-DD)
  closeDate: string | null;  // 登記閉鎖年月日 (null=存続中)
  process: string;           // 登記記録の閉鎖等の事由コード
  updateDate: string;        // 最終更新年月日
};

// search() 時のマッピング
//   results.length > 0 → status="hit",   hitCount=件数, summary="N件の法人情報が見つかりました"
//   results.length = 0 → status="clear", hitCount=0,    summary="該当する法人情報はありません"
//   例外                → status="error", hitCount=0,    summary=エラーメッセージ

運用メモ

  • closeDate が非 null の法人はリスクシグナル (閉鎖・解散)
  • 同名法人が複数の場合は address でフィルタ。フィルタ結果 0 件なら元の全件を返す (ユーザーに名寄せ判断を委ねる)

5.2 EDINET API

  • エンドポイント:
    • 書類一覧: https://api.edinet-fsa.go.jp/api/v2/documents.json
    • 書類本文: https://api.edinet-fsa.go.jp/api/v2/documents/{docID} (type=5 で CSV ZIP)
  • 認証: Subscription-Key (無償登録)
  • 用途: 上場企業の有価証券報告書から役員一覧を抽出し、追加の CheckEntity を生成する
  • レート制限: 3 秒/リクエスト (EDINET 規定)

入力

メソッド パラメータ 説明
search(entity) SearchEntity { name } 過去 N 日分の書類一覧を日次で走査し、filerNameentity.name を部分一致で含むもの収集
findDocuments(filerName, { days=30, docType? }) 日数・書類種別 docType="120" で有価証券報告書に絞込み
extractOfficers(docId) docId: string 書類本文 ZIP をダウンロードし、InformationAboutOfficersTextBlock の HTML テーブルから個人名を抽出

書類一覧リクエスト (日次):

GET /api/v2/documents.json?date={YYYY-MM-DD}&type=2&Subscription-Key={key}

出力

search()ScreeningOutcome.details.documents:

type EdinetDocument = {
  docID: string;            // 例: "S100ABCD"
  edinetCode: string;       // 例: "E00001"
  secCode: string | null;   // 証券コード (非上場は null)
  JCN: string | null;       // 法人番号 (13 桁)
  filerName: string;        // 提出者名
  docTypeCode: string;      // "120"=有価証券報告書
  docDescription: string;   // 「有価証券報告書-第XX期(...)」
  submitDateTime: string;   // ISO-8601
  periodStart: string;      // 対象期間
  periodEnd: string;
};

extractOfficers(docId)string[] (個人名の重複排除済み配列)

抽出ヒューリスティック:

  • XBRL_TO_CSV 内の全 CSV から InformationAboutOfficersTextBlock の HTML を取得
  • <td> テキストのうち 2〜20 文字、CJK 2 文字以上、役職語 (代表/取締役/監査/…) を含まないものを個人名とみなす

運用メモ

  • レート制限のため findDocuments は日付ループで 3 秒スリープを挟む。UI は進捗表示必須
  • docTypeCode=120 以外 (四半期報告書 160、臨時報告書 180) も将来対応可

5.3 官報 (KanpouClient)

  • 公式: https://search.npb.go.jp/ (有料)
  • 無料検索: https://search.kanpoo.jp/
  • 用途: 破産・解散・民事再生等の公告照合
  • 現状: 2025 年の個人情報保護改正により、反社チェックで重要な 破産・民事再生・免責許可決定は画像 PDF・90 日限定公開 となり機械的検索が不可

入力

メソッド パラメータ
search(entity) SearchEntity { name }
generateSearchUrl(entityName) string (kanpoo.jp 検索 URL を返すだけ)

出力

常に status: "manual_required" を返し、details で検索可能/不可能なカテゴリと参照 URL を提示する:

{
  source: "KANPOU",
  status: "manual_required",
  hitCount: 0,
  summary: "官報の自動検索は2025年の個人情報保護改正により制限されています。手動確認が必要です。",
  details: {
    searchableCategories: ["解散", "合併", "会社更生"],
    restrictedCategories: ["破産", "民事再生", "免責許可決定"],
    paidServiceUrl: "https://search.npb.go.jp/",
    freeSearchUrl: "https://search.kanpoo.jp/",
    note: "破産・民事再生は画像PDF(90日間限定公開)のため自動検索不可"
  },
  executedAt: Date
}

運用メモ

  • スクリーニング画面では「官報手動確認へ」ボタンで freeSearchUrl?q={name} を開き、担当者が件数・該当種別を入力する運用とする
  • 将来的に有料 API (NPB 有料官報情報検索サービス) 契約時は status: "hit"/"clear" を返す本格実装に差し替える

5.4 金融庁 (FSA) 行政処分

  • データソース: https://www.fsa.go.jp/status/s_jirei/s_jirei.xlsx (四半期更新、~2,800 行 × 12 列)
  • 方式: Excel を週次クローンで取得しローカルキャッシュ、名前部分一致で検索
  • 用途: 金融機関・無登録業者等への行政処分履歴の照合

入力

メソッド パラメータ 説明
search(entity) SearchEntity { name } キャッシュ未作成時は自動で refreshData() を呼ぶ
refreshData() なし Excel を取得して .cache/fsa-actions.xlsx に保存、行数を返す
findActions(companyName) string 金融機関等名 に小文字部分一致する行を返す

出力

search()ScreeningOutcome.details.actions:

type FsaAction = {
  fiscalYear: string;        // 年度
  publicationDate: string;   // 公表日 (YYYY-MM-DD)
  lifted: string;            // 解除の有無
  sector1: string;           // 業態1
  sector2: string;           // 業態2
  companyName: string;       // 金融機関等名
  corporateNumber: string;   // 法人番号 (13 桁、空欄あり)
  legalBasis: string;        // 根拠法令
  actionType: string;        // 処分の種類
  actionDetails: string;     // 処分の内容
  primaryCause: string;      // 主たる処分原因
  trigger: string;           // 主たる契機
};

// マッピング
//   matches.length > 0 → status="hit",   hitCount=件数, summary="N件の行政処分が見つかりました"
//   matches.length = 0 → status="clear", hitCount=0,    summary=null
//   例外                → status="error", hitCount=0,    summary=エラーメッセージ

運用メモ

  • corporateNumber 列が埋まっていれば法人番号での照合も可能 (SSOT の強化)
  • fsa-data-sync バックグラウンドジョブで週次 refreshData() を実行し、summary に「キャッシュ更新日」を表示

5.5 国土交通省 (MLIT) ネガティブ情報

  • エンドポイント: https://www.mlit.go.jp/nega-inf/cgi-bin/search.cgi (POST)
  • 方式: HTML スクレイピング (複数カテゴリに対して順次 POST)
  • 用途: 建設業・宅建業・マンション管理業等の行政処分履歴照合
  • レート制限: カテゴリ間 1.5 秒スリープ (既定)

入力

メソッド パラメータ
search(entity) SearchEntity { name }

内部では定義済みカテゴリ配列 CATEGORIES を順に POST する。各カテゴリごとに会社名フィールド名が異なるため、companyField と列マッピング columns を持つ:

type CategoryDef = {
  id: string;               // 例: "kensetugyousya"
  label: string;            // 例: "建設業者"
  companyField: string;     // POST 時の会社名フィールド名
  columns: (keyof MlitRecord | null)[]; // <td> の順序マッピング
};

対応カテゴリ (現状):

  • 建設業者 / 宅地建物取引業者 / マンション管理業者 / 賃貸住宅管理業者 / 指名停止 / 不動産鑑定業者 ほか

出力

search()ScreeningOutcome.details.records:

type MlitRecord = {
  category: string;          // カテゴリラベル (例: "建設業者")
  companyName: string;       // 商号又は名称
  address: string;           // 主たる営業所の所在地
  dispositionDate: string;   // 処分年月日
  authority: string;         // 処分を行った者
  dispositionType: string;   // 処分の種類
  detailUrl: string;         // 詳細ページ URL (相対 → 絶対化済み)
};

// マッピング
//   records.length > 0 → status="hit",   hitCount=件数, summary="N件のネガティブ情報が見つかりました (カテゴリ: X, Y)"
//   records.length = 0 → status="clear", hitCount=0,    summary=null
//   例外                → status="error", hitCount=0,    summary=エラーメッセージ

運用メモ

  • summary には該当カテゴリ名を列挙し、担当者が detailUrl を開いて詳細確認する導線を UI で提供する
  • HTML 構造変更に脆弱なため、E2E テストで週次検出する想定

6. 主要 UI 画面

画面レイアウトは実際の HTML ファイルで管理しています(SSOT)。以下のリンクからブラウザで確認できます。

6.1 ダッシュボード (/)

画面レイアウトを見る →

ステータスカード(未着手/審査中/要確認/完了)、期限アラート、最近のチェック一覧を表示。

6.2 チェック依頼詳細 — スクリーニング (/checks/[id]/screening)

画面レイアウトを見る →

Tier 1(公的 DB 自動チェック)、EDINET、Tier 2(日経テレコン+インターネット手動検索)のスクリーニング結果と操作パネル、最終判定ボタンを含む画面。


7. API ルート設計

Method Path 説明 権限
GET /api/checks 依頼一覧 (ステータス・期間でフィルタ) SALES+
POST /api/checks 依頼新規作成 SALES+
GET /api/checks/:id 依頼詳細 (エンティティ・結果含む) LEGAL+
PATCH /api/checks/:id 依頼更新 (ステータス・判定) LEGAL+
POST /api/checks/:id/entities エンティティ追加 LEGAL+
POST /api/checks/import CSV 一括取込 SALES+
POST /api/screening/:checkId/osint Tier 1 OSINT 一括実行 LEGAL+
POST /api/screening/:checkId/edinet EDINET チェック (上場) LEGAL+
POST /api/screening/:entityId/nikkei-result 日経テレコン結果登録 LEGAL+
POST /api/screening/:entityId/internet-result インターネット検索結果登録 LEGAL+
POST /api/screening/:entityId/ff-legal-review FF 法務レビュー登録 FF_LEGAL+
GET /api/houjin?name=...&address=... 法人番号参照 LEGAL+
GET /api/edinet?code=... EDINET 書類検索 LEGAL+
GET /api/export/checks/:id/pdf チェック結果 PDF 出力 LEGAL+
GET /api/export/ledger?from=...&to=... 検索結果一覧表 Excel 出力 LEGAL+
GET /api/keywords ネガティブキーワード一覧 LEGAL+
POST /api/keywords キーワード追加・更新 ADMIN
GET /api/monitoring/due 年次再チェック期限リスト LEGAL+

8. 認証・認可

認証

  • next-auth (または lucia-auth) によるセッション認証 (email/password)
  • セッションは PostgreSQL に保存
  • 30 分無操作タイムアウト

認可マトリクス

アクション SALES LEGAL FF_LEGAL COMPLIANCE ADMIN
依頼作成
自己依頼の閲覧
全依頼の閲覧
スクリーニング実行
日経テレコン結果入力
エスカレーション審査
判定記録
取引謝絶承認
キーワード管理
ユーザー管理
データエクスポート

9. ネガティブキーワード初期データ

業務手順書から抽出し、カテゴリ別に整理 (抜粋):

export const DEFAULT_KEYWORDS = [
  // 暴力団関連
  { word: "構成員", category: "organized_crime" },
  { word: "準構成", category: "organized_crime" },
  { word: "組員", category: "organized_crime" },
  { word: "組長", category: "organized_crime" },
  { word: "暴力団", category: "organized_crime" },
  { word: "総会屋", category: "organized_crime" },
  { word: "フロント企業", category: "organized_crime" },
  { word: "密接交際者", category: "organized_crime" },
  // ... (犯罪捜査 / 金融犯罪 / 薬物・武器カテゴリは EN 版参照)
] as const;

10. バックグラウンドジョブ

Bun worker threads によるノンブロッキング実行:

ジョブ トリガ 内容
osint-screening 依頼提出時 Tier 1 OSINT を並列実行
annual-recheck-alert 日次 cron 30 日以内に期限到来の依頼にリマインド送信
overdue-check-alert 日次 cron 閾値超過の未完了依頼を検出
fsa-data-sync 週次 cron 金融庁行政処分 Excel のキャッシュ更新

11. 実装フェーズ

Phase 1 — 基盤 (MVP)

  • スキャフォールディング (Bun + Next.js + Prisma + PostgreSQL)
  • データモデル・マイグレーション
  • ロールベース認証
  • チェック依頼 CRUD と状態遷移
  • エンティティ管理
  • ネガティブキーワード管理と初期データ

Phase 2 — OSINT スクリーニング (Tier 1)

  • 法人番号 API クライアントとエンティティ解決
  • 上場企業向け EDINET API クライアントと役員抽出
  • 官報検索クライアント
  • 金融庁行政処分ルックアップ
  • 国土交通省ネガティブ情報ルックアップ
  • 並列実行オーケストレーション

Phase 3 — 日経テレコン・インターネット検索 (Tier 2)

  • 日経テレコンクエリ生成器 + UI フロー
  • Google 検索 URL 生成器
  • 手動結果入力 UI
  • FF 法務エスカレーションフロー
  • 謝絶時の必須コメント付き判定記録

Phase 4 — 帳票・モニタリング

  • 検索結果一覧表 (Excel エクスポート)
  • 個別チェック PDF (監査証跡)
  • ダッシュボードウィジェットとアラート
  • 年次再チェック監視
  • 期限超過アラート

Phase 5 — 仕上げ

  • CSV 一括取込
  • メール通知
  • 監査ログビューア
  • データ保持ポリシー (5 年で自動アーカイブ)
  • E2E テスト・負荷テスト
  • Docker パッケージング

12. 技術選定

項目 選定 理由
ランタイム Bun ユーザー要件、高速起動、ネイティブ TypeScript
フレームワーク Next.js (App Router) Server Components でクライアント JS 削減、API Routes 内蔵
ORM Prisma 型安全、マイグレーション管理、PostgreSQL サポート
UI スタイリング Tailwind CSS ユーティリティファースト、高速反復
コンポーネント shadcn/ui アクセシブル、日本語フレンドリー
認証 next-auth v5 セッション管理、credential provider 内蔵
PDF 出力 @react-pdf/renderer React コンポーネントから監査 PDF 生成
Excel 出力 exceljs 既存スプレッドシート形式との整合
HTTP クライアント Bun 内蔵 fetch 追加依存なし
テスト Bun test + Playwright 単体/統合は bun:test、E2E は Playwright