設計書 — 反社会的勢力チェックアプリケーション
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/";
}
}
スクリーニング画面での業務フロー:
- システムが生成済みクエリと「日経テレコンを開く」ボタンを表示
- 担当者がログインして記事検索へ遷移、保存済み検索条件を実行
- 「絞込み」にシステム提示のエンティティ名を入力
- ヒット件数をアプリに入力
- 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 日分の書類一覧を日次で走査し、filerName に entity.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 |