0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【WordPress × D3.js】5類型34パターンの契約書リスク検出ツールをフロントエンド完結で実装した

0
Posted at

ai-legal-review.jpg

この記事でできること

  • フロントエンド完結型(サーバー送信なし)のテキスト解析ツールの基本設計が分かる
  • D3.jsをWordPressに組み込む際の3つの必須対応(CDNタグ禁止・&&禁止・wp:htmlラッパー)が分かる
  • キーワードパターンマッチングによる文書タイプ自動判定とリスクスコアリングの実装手順が分かる
  • 実際に動作するツールはAI法務文書レビュー(AI Japan Index)で確認できる

環境・前提

  • ブラウザ: Chrome 123+ / Firefox 125+ / Safari 17+
  • D3.js: v7(CDNからの動的ロード)
  • WordPress: 6.5以降(SWELLテーマ使用環境を想定)
  • JavaScript: ES2020(Vanilla)
  • 追加のビルドツールなし(webpack/Vite等不要)

完成形

NDA・業務委託・雇用・売買・利用規約(事業者向け)の5類型を自動判定し、34パターンのリスク条項を検出してA〜Eグレードで評価するツール。入力テキストはブラウザ内のみで処理され、サーバーには一切送信されない。動作確認はAI法務文書レビューで行える。


手順

Step 1: WordPressでD3.jsを動的ロードする(CDNタグは使えない)

WordPressのコンテンツ内で <script src="CDN"> を使ってはいけない。WordPress/SWELLはコンテンツ内の外部スクリプトタグを無視するため、D3.jsが読み込まれずに描画が一切されない。

NG例(WordPress環境では動かない):

<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
<script>
  // d3 is undefined → エラー
  var svg = d3.select("#chart");
</script>

OK例(動的ロードで解決):

function loadD3(callback) {
  if (typeof d3 !== "undefined") {
    callback();
    return;
  }
  var script = document.createElement("script");
  script.src = "https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js";
  script.onload = callback;
  script.onerror = function() {
    document.getElementById("legal-review-error").style.display = "block";
  };
  document.head.appendChild(script);
}

document.addEventListener("DOMContentLoaded", function() {
  loadD3(initLegalReview);
});

typeof d3 でロード済みかを確認し、未ロードなら createElement("script") で動的に追加する。onload で描画初期化関数を呼び出す。

Step 2: && 演算子を三項演算子に置き換える

WordPressのコンテンツフィルターは &&&#038;&#038; にHTMLエンティティ変換する。これはJSの構文エラーになりスクリプト全体が停止する。

NG例(WordPressで壊れる):

const label = isMobile && hasRisk ? "リスクあり" : "問題なし";
const margin = { left: isMobile && hasLabel ? 240 : 15 };

OK例(三項演算子またはネストifで代替):

var isMob = isMobile ? true : false;
var label = isMob ? (hasRisk ? "リスクあり" : "問題なし") : "問題なし";
var leftMargin = isMob ? 15 : 240;
var margin = { left: leftMargin };

つまずきポイント: この変換はWordPressの管理画面から投稿を保存するタイミングで発生する。ローカルHTMLでは正常動作するのに、WordPress投稿後だけ動かない——という現象はこれが原因。

Step 3: 文書タイプの自動判定ロジックを実装する

入力テキストに対して5類型のキーワード出現頻度をスコアリングし、最高スコアの文書タイプを選定する。

var docTypeKeywords = {
  nda: ["秘密保持", "秘密情報", "開示", "NDA", "守秘義務", "秘密保持契約"],
  commission: ["業務委託", "委託料", "成果物", "納品", "業務委託契約", "請負"],
  employment: ["雇用", "就業", "給与", "労働", "従業員", "雇用契約"],
  sale: ["売買", "商品", "代金", "納入", "売買基本契約", "購入"],
  tos: ["利用規約", "サービス", "アカウント", "ユーザー", "禁止事項", "免責"]
};

function detectDocType(text) {
  var scores = {};
  Object.keys(docTypeKeywords).forEach(function(type) {
    scores[type] = 0;
    docTypeKeywords[type].forEach(function(kw) {
      if (text.indexOf(kw) !== -1) {
        scores[type] += 1;
      }
    });
  });
  return Object.keys(scores).reduce(function(a, b) {
    return scores[a] >= scores[b] ? a : b;
  });
}

ポイント: キーワードが1件もヒットしない場合は複数のタイプが同スコアになる可能性がある。ユーザーが手動でタイプを選択できるUIを併用することで補完する。

Step 4: リスクパターンマッチングとグレード計算を実装する

文書タイプのリスクパターンリストを走査し、ヒットした条項を収集する。グレード計算式は「高リスク×3+中リスク×2+低リスク×1」のスコアに基づく。

var riskPatterns = {
  commission: [
    {
      id: "ip_transfer",
      name: "知的財産権の一方帰属",
      level: "high",
      keywords: ["著作権", "知的財産権", "特許権", "実用新案", "意匠権", "商標権"],
      description: "成果物の著作権等が発注者に一方帰属する条項。受注者の二次利用不可。",
      law: "著作権法・フリーランス新法"
    },
    {
      id: "payment_delay",
      name: "60日超の支払条件",
      level: "high",
      keywords: ["60日", "90日", "120日", "手形", "支払期日"],
      description: "下請法では支払期日は60日以内が義務。フリーランス新法でも同様の規定あり。",
      law: "下請法第2条の2・フリーランス新法"
    }
    // 他のパターンを追加
  ]
};

function analyzeText(text, docType) {
  var patterns = riskPatterns[docType];
  if (!patterns) { return []; }
  var detected = [];
  patterns.forEach(function(pattern) {
    var hit = false;
    pattern.keywords.forEach(function(kw) {
      if (text.indexOf(kw) !== -1) { hit = true; }
    });
    if (hit) { detected.push(pattern); }
  });
  return detected;
}

function calcGrade(detected) {
  var high = 0, mid = 0, low = 0;
  detected.forEach(function(item) {
    if (item.level === "high") { high += 1; }
    else if (item.level === "mid") { mid += 1; }
    else { low += 1; }
  });
  var score = high * 3 + mid * 2 + low * 1;

  if (score < 2) { return "A"; }
  if (score <= 3) { return "B"; }
  if (score <= 6) { return "C"; }
  if (score <= 9) { return "D"; }
  return "E";
}

Step 5: D3.jsで横棒チャートを描画する(スマホ対応含む)

検出結果をD3.jsの横棒チャートで可視化する。スマホ(767px以下)ではY軸ラベルを非表示にし、バーのタッチで下部固定パネルに詳細を表示する。

function drawRiskChart(detected) {
  var isMobile = window.innerWidth <= 767;
  var margin = {
    top: 20, right: 60,
    bottom: 40,
    left: isMobile ? 15 : 240
  };
  var container = document.getElementById("risk-chart");
  var width = container.clientWidth;
  var height = Math.max(200, detected.length * 44 + margin.top + margin.bottom);
  var innerWidth = width - margin.left - margin.right;
  var innerHeight = height - margin.top - margin.bottom;

  var colorMap = { high: "#c0392b", mid: "#e67e22", low: "#f1c40f" };

  var svg = d3.select("#risk-chart")
    .append("svg").attr("width", width).attr("height", height);

  var g = svg.append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  var yScale = d3.scaleBand()
    .domain(detected.map(function(d) { return d.id; }))
    .range([0, innerHeight]).padding(0.3);

  var xScale = d3.scaleLinear().domain([0, 3]).range([0, innerWidth]);

  var bars = g.selectAll(".risk-bar")
    .data(detected).enter().append("rect")
    .attr("class", "risk-bar")
    .attr("x", 0)
    .attr("y", function(d) { return yScale(d.id); })
    .attr("width", function(d) {
      return xScale(d.level === "high" ? 3 : (d.level === "mid" ? 2 : 1));
    })
    .attr("height", yScale.bandwidth())
    .attr("fill", function(d) { return colorMap[d.level]; });

  // PCのみY軸ラベル表示
  if (!isMobile) {
    g.selectAll(".y-label").data(detected).enter().append("text")
      .attr("class", "y-label")
      .attr("x", -8)
      .attr("y", function(d) { return yScale(d.id) + yScale.bandwidth() / 2; })
      .attr("text-anchor", "end").attr("dominant-baseline", "middle")
      .text(function(d) { return d.name; });
  }

  // スマホ: タッチで下部固定パネルに詳細を表示
  bars.on("touchstart", function(event, d) {
    event.preventDefault();
    event.stopPropagation();  // ← 省略するとパネルが開いて即閉じる
    showMobilePanel(d.name, d.level, d.description, d.law);
  });
}

つまずきポイント(scaleBand): yScale.domain() に渡すキーと、バー配置の .attr("y", function(d) { return yScale(d.???) }) のキーが一致していないとバーが描画されない。d.id でドメインを作ったなら、アクセサも yScale(d.id) にする。

つまずきポイント(タッチパネル): e.stopPropagation() を省略すると「パネルが表示された直後に閉じる」現象が起きる。バーのタッチイベントがdocumentまで伝播し「パネル外タップ」と誤判定されるためだ。必ずセットで書く。

Step 6: wp:htmlラッパーで全体をまとめる(DOCTYPE禁止)

WordPressへの貼り付けは <!-- wp:html --> ブロックで全体を囲む。<!DOCTYPE html><html> タグから始まるフルHTML構造は使わない(wpautopフィルターがscript内に <p> タグを挿入してJSを破壊する)。

<!-- wp:html -->
<div class="aji-legal-review-wrap">
  <style>
    /* 全セレクタを .aji-legal-review-wrap でスコープ */
    /* body{} h1{} 等のグローバルセレクタ禁止(SWELLテーマを破壊する) */
    .aji-legal-review-wrap textarea { width: 100%; min-height: 200px; }
    .aji-legal-review-wrap .grade-A { color: #27ae60; }
    .aji-legal-review-wrap .grade-E { color: #c0392b; }
  </style>

  <textarea id="contract-input" placeholder="契約書テキストを貼り付けてください"></textarea>
  <button id="analyze-btn">リスク分析を実行</button>
  <div id="grade-display" style="display:none;"></div>
  <div id="risk-chart"></div>
  <div id="mobile-panel" style="display:none; position:fixed; bottom:0; left:0;
    width:100%; background:#1a1a2e; border-top:2px solid #9369A8;
    padding:12px 16px; z-index:10000; max-height:40vh; overflow-y:auto;">
    <button id="panel-close">×</button>
    <div id="panel-content"></div>
  </div>

  <script>
    (function() {
      // D3動的ロード + 全処理コード
    })();
  </script>
</div>
<!-- /wp:html -->

つまずきポイント: body {}h1 {} 等のグローバルCSSセレクタを使うとSWELLテーマ全体のスタイルが変わる。全セレクタを .aji-legal-review-wrap で囲むことが必須。


つまずきポイントまとめ

  • D3が動かない(WordPress): <script src="CDN"> はWordPressに無視される → 動的ロードに変更
  • && の後でスクリプトが止まる: WordPressが &&&#038;&#038; に変換 → 三項演算子に置換
  • バーが描画されない(scaleBand): ドメインのキーとアクセサのキーが不一致 → 同じキーを使う
  • タッチパネルが即閉じる: e.stopPropagation() が抜けている → タッチイベントに必ず追加
  • スマホでラベルが重なる: 日本語10〜15文字はスマホ幅に入らない → if (!isMobile) で非表示にしてタッチパネル表示に切り替え
  • スタイルが全ページに影響する: グローバルCSSを使っている → .aji-legal-review-wrap でスコープを限定
  • 文書タイプが誤判定される: キーワード不足のケースがある → 手動選択UIを併用して補完

まとめ

  • フロントエンド完結型パターンマッチングで、サーバー送信なしの5類型34パターンリスク検出を実現できた
  • && 禁止・CDNタグ禁止・wp:htmlラッパーはWordPress/SWELLでのJS実装必須3点
  • D3.js v7のSVG描画でChart.jsのWordPress競合問題を回避した
  • scaleBandはドメインとアクセサを同一キーで統一する
  • e.stopPropagation() なしのタッチイベントはパネルが即閉じる

実装の動作確認はAI法務文書レビュー(AI Japan Index)で行える。同種のデータビジュアライゼーションツールはAI Japan Indexのツール一覧にまとまっている。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?