開発情報・ナレッジ

投稿者: ShiningStar株式会社 2026年4月24日 (金)

フォームに金融機関・支店の検索機能を組み込むサンプルプログラム(BankcodeJP連携)

フォーム上で金融機関・支店を検索一覧から選べるようにし、選択結果を項目へ格納する構成を紹介します。
検索用の中継は、フォームとは別にSPIRAL内にもう1つページを作成し、そのPHPタブ・BODYタブに貼り付ける前提です。ページのURLがそのままブラウザから叩ける中継エンドポイントになります。入力フォーム側の BODYタブ には検索UIのマークアップ、JSタブ にはその動作JSを貼り付けます。
検索処理は BankcodeJP API を利用します。

注意点

APIキーは
ページのPHP
(サーバ側)に保持し、
JSコード
(クライアント)には置きません。
中継用ページは、PHPを貼り付けるためだけの空ページとして運用してください。
無料プランには1日あたりのリクエスト上限などがあります。429 が返った場合は間隔を空ける、プランを見直す、など公式ドキュメントに沿って対応してください(API Documentation)。
curl
のタイムアウト(接続・全体)は短めに設定し、外部API遅延で画面が固まらないようにしてください。

実装の概要

事前準備. BankcodeJP で APIキー取得。SPIRAL内に「中継用ページ」を1つ作成し、その
PHPタブ
PHPコード
BODYタブ
HTMLコード
を貼り付けます。
本来の入力フォーム(銀行を選ばせたい画面)の
BODYタブ
HTMLコード
JSタブ
JSコード
を貼り付けます。
1. 入力フォーム側の
JSコード
が、ユーザー入力を base64 化して中継ページのURLへ
fetch
する。
2. 中継ページの PHPタブ(
PHPコード
)が base64 を復号して入力を検証し、
curl
で BankcodeJP を呼び、結果を
$SPIRAL->setTHValue('bankcodejp_result', base64(json))
で Thymeleaf に渡す。
3. 中継ページの BODYタブ(
HTMLコード
)の Thymeleaf が、渡された値を
<div id="bankcodejp-data" th:attr="data-result=...">
の data 属性に埋め込む。
4. 呼び出し側の JS(
JSコード
)が、
DOMParser
で受信HTMLから
#bankcodejp-data
data-result
属性を取り出し、base64 復号 → JSON.parse して候補一覧を
HTMLコード
のUI内に表示する。
5. 候補を選んだら、既存のフォーム入力項目(
f01〜f04
)に値を書き込み、送信する。

事前準備

以下を用意します。識別名は環境に合わせて読み替えてください。

用途識別名(例)フィールドタイプ
金融機関コード(4桁・数字のみ)
bank_code
テキスト(最大128字)
金融機関名
bank_name
テキスト(最大128字)
支店コード(3桁・数字のみ)
branch_code
テキスト(最大128字)
支店名
branch_name
テキスト(最大128字)
フィルタ構文の詳細は BankcodeJP API Documentation
filter
を参照してください。

設定方法

1. 中継ページのPHPタブ(curl で BankcodeJP を呼び、結果を Thymeleaf に渡す)
SPIRAL内に中継用のページを作成し、その PHPタブ に以下を貼り付けます。
curl_*
を用いて外部HTTPを行い、結果は
$SPIRAL->setTHValue('bankcodejp_result', base64_encode(json_encode(...)))
で Thymeleaf に渡します。

<?php
/**
 * SPIRAL ver.2 で、検索用の中継「PHPタブ」に貼り付けるコード。
 *
 */

$BANKCODEJP_API_KEY  = '***REPLACE_WITH_YOUR_KEY***';
$BANKCODEJP_API_BASE = 'https://apis.bankcode-jp.com/v3';

$response = null;

try {
    // SPIRAL ver.2 の PHPタブでは、$_GET は使えず $SPIRAL->getParam() で
    // GET/POST パラメータを取得する必要がある。
    $mode = (string)($SPIRAL->getParam('mode') ?? '');

    // q はJS側で UTF-8 → base64 にエンコードして渡してもらう。
    $qRaw = (string)($SPIRAL->getParam('q') ?? '');
    $q    = $qRaw === '' ? '' : (string)base64_decode($qRaw, true);
    $q    = trim($q);
    $bank = preg_replace('/\D/', '', (string)($SPIRAL->getParam('bank') ?? ''));

    if ($q === '' || mb_strlen($q) > 32) {
        throw new RuntimeException('invalid q');
    }
    $safe = preg_replace('/[^\x{3041}-\x{3096}A-Za-z0-9\-]/u', '', $q);
    if ($safe === null || $safe === '') {
        throw new RuntimeException('invalid characters');
    }

    $filter = 'hiragana==' . $safe . '*';
    $query  = http_build_query([
        'apikey' => $BANKCODEJP_API_KEY,
        'limit'  => 30,
        'filter' => $filter,
        'fields' => 'code,name,hiragana',
    ]);

    if ($mode === 'bank') {
        $url = $BANKCODEJP_API_BASE . '/banks?' . $query;
    } elseif ($mode === 'branch' && strlen($bank) === 4) {
        $url = $BANKCODEJP_API_BASE . '/banks/' . $bank . '/branches?' . $query;
    } else {
        throw new RuntimeException('invalid mode or bank');
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
    $body     = curl_exec($ch);
    $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlErr  = curl_error($ch);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException('upstream unreachable: ' . $curlErr);
    }
    if ($httpCode >= 400) {
        throw new RuntimeException('upstream status ' . $httpCode);
    }

    $decoded = json_decode((string)$body, true);
    if (!is_array($decoded)) {
        throw new RuntimeException('invalid upstream json');
    }

    $response = ['ok' => true, 'status' => 200, 'data' => $decoded];
} catch (Throwable $e) {
    $response = ['ok' => false, 'status' => 400, 'error' => $e->getMessage()];
}

// Thymeleaf に base64(json) 文字列として渡す。
// HTMLタブ(code1.html)側で data 属性に埋め込まれ、呼び出し側 JS から取り出される。
$json = json_encode($response, JSON_UNESCAPED_UNICODE);
$b64  = base64_encode($json);
$SPIRAL->setTHValue('bankcodejp_result', $b64);

2. 中継ページのBODYタブ(Thymeleaf で data 属性に埋め込む)
同じページの BODYタブ に以下を貼り付けます。
th:attr="data-result=${cp.result.value['bankcodejp_result']}"
で、
PHPタブから渡された base64 文字列を
<div id="bankcodejp-data">
data-result
属性に埋め込みます。

<div id="bankcodejp-data"
     th:data-result="|${cp.result.value['bankcodejp_result']}|"></div>
<pre id="bankcodejp-text" style="display:none;"
     th:text="${cp.result.value['bankcodejp_result']}"></pre>

3. 入力フォームのBODYタブ(検索UIマークアップ)
銀行を選ばせたい本来のフォームブロックの BODYタブ、デフォルトソースの
<div class="sp-form-container">
内に、以下の検索UIマークアップを貼り付けます。

<div class="sp-form-container">

  <!-- 銀行検索UI -->
  <div class="sp-form-item sp-form-html" th:inline="none">
    <p><span style="font-size: 18pt;">銀行検索フォーム</span></p>

    <div class="sp-form-field">
      <div class="sp-form-label">金融機関検索(ひらがな前方一致)</div>
      <div class="sp-form-data">
        <input type="text" id="bank-q" placeholder="例: みずほ" maxlength="32">
        <button type="button" id="bank-search-btn">検索</button>
        <ul id="bank-list"></ul>
        <p id="bank-selected" style="display:none;">
          選択中: <strong id="bank-selected-name"></strong>(<span id="bank-selected-code"></span>)
          <button type="button" id="bank-reset-btn">変更</button>
        </p>
      </div>
    </div>

    <div class="sp-form-field" id="branch-search-area" style="display:none;">
      <div class="sp-form-label">支店検索(ひらがな前方一致)</div>
      <div class="sp-form-data">
        <input type="text" id="branch-q" placeholder="例: しんじゅく" maxlength="32">
        <button type="button" id="branch-search-btn">検索</button>
        <ul id="branch-list"></ul>
        <p id="branch-selected" style="display:none;">
          選択中: <strong id="branch-selected-name"></strong>(<span id="branch-selected-code"></span>)
        </p>
      </div>
    </div>

    <p class="sp-form-error" id="bank-search-msg"></p>
  </div>

  <!--/* 金融機関コード(4桁・数字のみ)(bank_code) */-->
  <sp:input-field name="f01"></sp:input-field>
  <input type="hidden" id="f-bank-code"   th:name="${fields['f01'].name}" th:value="${inputs['f01']}">

  <!--/* 金融機関名(bank_name) */-->
  <sp:input-field name="f02"></sp:input-field>
  <input type="hidden" id="f-bank-name"   th:name="${fields['f02'].name}" th:value="${inputs['f02']}">

  <!--/* 支店コード(3桁・数字のみ)(branch_code) */-->
  <sp:input-field name="f03"></sp:input-field>
  <input type="hidden" id="f-branch-code" th:name="${fields['f03'].name}" th:value="${inputs['f03']}">

  <!--/* 支店名(branch_name) */-->
  <sp:input-field name="f04"></sp:input-field>
  <input type="hidden" id="f-branch-name" th:name="${fields['f04'].name}" th:value="${inputs['f04']}">

  <!-- 送信ボタン(デフォルトソースのまま) -->
  <div class="sp-form-item sp-form-interaction">
    <button class="sp-form-prev-button" type="submit" name="action" value="previous" th:if="!${step.isFirst}" th:text="${step.prevButtonLabel}">Prev</button>
    <button class="sp-form-next-button" type="submit" name="action" value="next" th:text="${step.nextButtonLabel}">Next</button>
  </div>
</div>

4. 入力フォームのJSタブ(fetch で中継ページを呼び、data 属性から結果を取り出す)
フォームブロックを設置したページの JSタブ に以下のJSを貼り付けます。
fetch
したレスポンスHTMLを
DOMParser
で解析し、
#bankcodejp-data
data-result
属性から base64 文字列を取り出して復号します。
BANK_PROXY_URL
は中継用ページのURLに差し替えてください。
※ JSタブ内では Thymeleaf インライン式(
[[${...}]]
)は展開されないため、本JSは
DOMParser
経由で BODYタブ側の data 属性を読む方式で書かれています。本サンプルは ステップ構成 (Step1 =
s01
)
f01〜f04
を対象としており、入力項目は
[name="s01.f01"]
優先・
[name="f01"]
フォールバックで取得します。ステップIDやフィールドIDが異なる環境では JS 内の
STEP
定数と
findField
の引数を読み替えてください。


// PHPコードを貼り付けた中継ページのフルURL(環境に合わせて差し替え)
var BANK_PROXY_URL = 'https://example.com/path/to/bankcodejp_proxy_page';

// ---- base64(UTF-8対応)ユーティリティ ----
function b64EncodeUtf8(str) {
  var bytes = new TextEncoder().encode(str);
  var bin = '';
  for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
  return btoa(bin);
}
function b64DecodeUtf8(b64) {
  var bin = atob(b64);
  var bytes = new Uint8Array(bin.length);
  for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return new TextDecoder('utf-8').decode(bytes);
}

async function callBankProxy(params) {
  // BANK_PROXY_URL に既に ? が含まれている場合は & で繋ぐ
  var sep = BANK_PROXY_URL.indexOf('?') >= 0 ? '&' : '?';
  var sendParams = Object.assign({}, params);
  if (typeof sendParams.q === 'string') sendParams.q = b64EncodeUtf8(sendParams.q);
  var url = BANK_PROXY_URL + sep + new URLSearchParams(sendParams).toString();
  var res = await fetch(url, { credentials: 'same-origin' });
  if (!res.ok) throw new Error('proxy http ' + res.status);

  // 中継ページの BODYタブ (code2.html) が Thymeleaf で値を埋め込んだ HTML を返す前提。
  // data属性方式 と th:text方式の両方を試して、どちらかで取得できればそれを使う。
  var html = await res.text();
  var doc  = new DOMParser().parseFromString(html, 'text/html');

  // 1) data-result 属性から取得(推奨・公式方法③)
  var b64 = '';
  var dataEl = doc.getElementById('bankcodejp-data');
  if (dataEl) {
    b64 = (dataEl.getAttribute('data-result') || '').trim();
  }

  // 2) フォールバック: th:text で書き出した <pre id="bankcodejp-text"> の textContent
  if (!b64) {
    var textEl = doc.getElementById('bankcodejp-text');
    if (textEl) {
      b64 = (textEl.textContent || '').trim();
    }
  }

  if (!b64) {
    // 切り分け用にレスポンス全体と、見つかった要素の状態を console に出す
    console.log('[bankcodejp] raw response:', html);
    console.log('[bankcodejp] #bankcodejp-data:', dataEl ? dataEl.outerHTML : '(not found)');
    throw new Error('Thymeleaf の値が空です。PHPタブの $SPIRAL->setTHValue が効いているか、BODYタブのキー名 (bankcodejp_result) が一致しているか確認してください。');
  }

  var payload;
  try {
    payload = JSON.parse(b64DecodeUtf8(b64));
  } catch (err) {
    throw new Error('invalid base64/json in data-result');
  }
  if (!payload.ok) throw new Error(payload.error || 'proxy error');
  return payload.data;
}

async function fetchBanks(hiraganaPrefix) {
  var safe = (hiraganaPrefix || '').replace(/[^\u3041-\u3096a-zA-Z0-9\-]/g, '');
  if (!safe) throw new Error('invalid input');
  return callBankProxy({ mode: 'bank', q: safe });
}

async function fetchBranches(bankCode4, hiraganaPrefix) {
  var safe = (hiraganaPrefix || '').replace(/[^\u3041-\u3096a-zA-Z0-9\-]/g, '');
  if (!safe) throw new Error('invalid input');
  if (!/^\d{4}$/.test(bankCode4)) throw new Error('invalid bank code');
  return callBankProxy({ mode: 'branch', q: safe, bank: bankCode4 });
}

// ---- UI 配線(DOM ready 後)----
document.addEventListener('DOMContentLoaded', function () {
  var $ = function (id) { return document.getElementById(id); };
  var msgEl = $('bank-search-msg');
  var msg = function (t) { if (msgEl) msgEl.textContent = t || ''; };

  // BODYタブ (code3.html) で配置した hidden input を固定IDで取得する。
  // th:name="${fields['fNN'].name}" により runtime の name 属性は
  // SPIRAL 側の仕様(ステップ prefix の有無)に自動追従するため、
  // JS 側はステップ名を意識する必要がない。
  var bankCodeInput   = $('f-bank-code');
  var bankNameInput   = $('f-bank-name');
  var branchCodeInput = $('f-branch-code');
  var branchNameInput = $('f-branch-name');

  // 選択状態は JS 内部で保持
  var selectedBankCode = '';
  var selectedBankName = '';

  function renderList(listEl, items, onPick) {
    listEl.innerHTML = '';
    if (!items || items.length === 0) {
      listEl.innerHTML = '<li>該当なし</li>';
      return;
    }
    items.forEach(function (it) {
      var li = document.createElement('li');
      var btn = document.createElement('button');
      btn.type = 'button';
      btn.textContent = it.name + '(' + it.code + ')';
      btn.addEventListener('click', function () { onPick(it); });
      li.appendChild(btn);
      listEl.appendChild(li);
    });
  }

  var bankSearchBtn = $('bank-search-btn');
  if (bankSearchBtn) bankSearchBtn.addEventListener('click', async function () {
    msg('');
    try {
      var data  = await fetchBanks($('bank-q').value);
      var items = data.banks || data.items || data || [];
      renderList($('bank-list'), items, function (bank) {
        selectedBankCode = String(bank.code || '');
        selectedBankName = String(bank.name || '');
        if (bankCodeInput) bankCodeInput.value = selectedBankCode;
        if (bankNameInput) bankNameInput.value = selectedBankName;
        $('bank-selected-name').textContent = selectedBankName;
        $('bank-selected-code').textContent = selectedBankCode;
        $('bank-selected').style.display = '';
        $('branch-search-area').style.display = '';
        $('bank-list').innerHTML = '';
      });
    } catch (e) { msg('検索に失敗しました: ' + e.message); }
  });

  var bankResetBtn = $('bank-reset-btn');
  if (bankResetBtn) bankResetBtn.addEventListener('click', function () {
    selectedBankCode = '';
    selectedBankName = '';
    if (bankCodeInput)   bankCodeInput.value = '';
    if (bankNameInput)   bankNameInput.value = '';
    if (branchCodeInput) branchCodeInput.value = '';
    if (branchNameInput) branchNameInput.value = '';
    $('bank-selected').style.display = 'none';
    var brSel = $('branch-selected'); if (brSel) brSel.style.display = 'none';
    $('branch-search-area').style.display = 'none';
    $('branch-list').innerHTML = '';
  });

  var branchSearchBtn = $('branch-search-btn');
  if (branchSearchBtn) branchSearchBtn.addEventListener('click', async function () {
    msg('');
    if (!/^\d{4}$/.test(selectedBankCode)) { msg('先に金融機関を選んでください'); return; }
    try {
      var data  = await fetchBranches(selectedBankCode, $('branch-q').value);
      var items = data.branches || data.items || data || [];
      renderList($('branch-list'), items, function (br) {
        var code = String(br.code || '');
        var name = String(br.name || '');
        if (branchCodeInput) branchCodeInput.value = code;
        if (branchNameInput) branchNameInput.value = name;
        $('branch-selected-name').textContent = name;
        $('branch-selected-code').textContent = code;
        $('branch-selected').style.display = '';
        $('branch-list').innerHTML = '';
      });
    } catch (e) { msg('検索に失敗しました: ' + e.message); }
  });
});

エラーハンドリング(失敗時の動き)

BankcodeJPから 429 が返った場合は、
PHPコード
ok:false
を返し、
JSコード
が「時間をおいて再試行」を表示するなど、ユーザーが手入力にフォールバックできるようにする。
curl
タイムアウト・接続エラー時は
ok:false
を返し、画面ロードをブロックしない。

実行結果

フォーム上で金融機関・支店を選択すると、当該項目にコードと名称が格納され、登録される。

まとめ

本記事では、SPIRAL内で完結する構成として、中継用のページに
curl
でBankcodeJPを呼ぶPHPを貼り付け、フォーム側のJSからそのページに
fetch
する流れを示しました。

解決しない場合はこちら コンテンツに関しての
要望はこちら