NGW TECH BLOG
← 記事一覧へ
·#093

誰も使わなかった「決裁文書設定」を掘り起こしたら、ノーコードの申請フォームになった

誰も使っていないはずの機能を掘り起こしたら、動的フォームも起案もPDFも既にできていて、止まっていたのはセレクトボックスを囲むHTMLコメント1つだった。復活は1行、手間はその周りに全部あった。

直したのはコメントアウトを1つ外しただけだった。それなのに、そこへ辿り着くまでと、辿り着いたあとが長かった。

NGWLinux の「決裁文書設定」という画面を、ずっと前に見た記憶があった。休暇マスターと同じ @ 区切りのDSL(Domain Specific Language、ある用途だけのために作った独自の小さな記法。ここでは「1行で1項目」を表す)で、決裁書類の項目を自由に定義できる仕組みらしい。ただし記憶の中では、それは誰も使っていない機能だった。

今回、別件で決裁書類まわりを触っていて、この設定画面をもう一度開いた。動くかどうかも分からないまま、コードを掘ってみることにした。

掘ってみたら、全部あった

マスターのCRUD(Create・Read・Update・Delete、登録・参照・更新・削除の4操作。管理画面の基本セット)、DSLから動的フォームを組み立てる処理(fncMakeWfdBody)、起案・決裁・取下げ・PDF出力、業務ごとの分岐が入った通知処理まで、一通り実装済みだった。休暇マスター(jinjKyuka.php)を原型に、同じ項目定義DSLを流用して作られていた。

無かったのはこの5つだけだった。

  1. テンプレートを選ぶセレクトボックス — HTMLコメント <!-- --> で丸ごと無効化され、項目IDが -1 に固定されていた
  2. 決裁完了時に所管へ通知する処理
  3. 所管から申請者へ書類を返す経路
  4. マスターの有効・無効を切り替える仕組み
  5. 保存先テーブルの CREATE TABLE — リポジトリのどのSQLにも入っていなかった

つまり、画面としては完成していたのに、入口のセレクトボックスがコメントで塞がれていた。これが冒頭の「復活は1行」の正体である。既存データ27,000件超はすべて自由記述(テンプレート未選択)で決裁されていたので、「テンプレート未選択なら従来通りの自由記述」を残し、選択肢に「自由記述」という項目は並べないことにした。今まで通り自由に書きたい人には、今まで通りの画面を見せる。

復活は1行、地雷は索引にあった

セレクトを開放するだけなら数分の作業だった。腰を据えたのは、その先で見つけた索引のズレのほうだった。

このDSLは NAME@表示名@TYPE@高さ@選択肢 のように @ で区切った1行で1項目を表す。ところが、@ で区切った何番目が何を意味するかが、画面とPDFと原型のマスターとでバラバラだった。

  • 決裁申請設定の画面は SUBJECT/RADIO/CHECK を4番目、REM/TEXT_AREA を3番目から読む
  • 原型の休暇申請は RADIO を3番目から読む
  • マスターのヘルプ文言は3番目の例しか出していない
  • PDF側は3番目(高さ)を省略できない数値として扱う

この最後が致命的だった。RADIO@@… のように高さを省いて書くと float * '' で型エラーになり、DATE1@使用日@DATE のように3番目が無い行はエラーも出さず黙って落ちる。決裁画面のPDFプレビューが真っ黒になっていた原因はこれだった。

直したのは、PDF側は高さ未指定なら1行として扱う、4番目はisset()で存在チェックする、ヘルプ文言は「3番目は高さ。省略時は1、4番目まで使うなら3番目は @@ と空にする」で統一する、の3つ。だが直しながら気づいた。この書式を人に手で書かせる限り、この手の食い違いはまた起きる。 @@ の空欄を正しく数えて書ける人は少ない。

ビルダーを被せることにした本当の理由はここにある。保存形式はDSLの1行テキストのまま変えない。ビルダーは、ブロックをドラッグ&ドロップで並べたら、その裏で同じDSLの1行を書き出すだけの編集画面にした。実行側もPDF側も無改造で済む。ライブラリも追加していない。画面には既に読み込まれていた jQuery UI 1.11 の sortable / draggable がそのまま使えた。

既存のDSLをビルダーに読み込んで、保存し直して、もう一度DSLとして書き出す。それを決裁申請・休暇マスターの両方の既存データで試したところ、1文字も変わらなかった。往復して差分が出ないことを確認できて、初めて置き換えていいと判断できた。

実際に組んでみたテンプレートの1つが、戸籍の公用申請だった。DSLで書けばこの6行になる。

TXA1@事業名@TEXT_AREA@2
TXT2@補助事業名@TEXT@1
TXT3@必要な人の本籍@TEXT@1
SEL1@謄本・抄本@RADIO@1@1:謄本,2:抄本
TXT4@筆頭者氏名@TEXT@1
DT1@受領希望日@DATE@1

ビルダーに載せると、こうなる。

決裁申請設定のノーコードビルダー画面。上部に申請名「戸籍公用申請」の入力欄と公開/下書きのラジオボタン、連携処理として「所管へ完了通知(未読回覧)」のチェックと通知先「所管全員」のセレクトがある。項目設定の欄には、注記・1行入力・複数行入力・日付・日時・期間・時刻・時間帯・選択(1つ)・選択(複数)・その他のボタンが並び、その下に積み上げ式のブロックが6個: 複数行入力TXA1「事業名」(行数2)、1行入力TXT2「補助事業名」、1行入力TXT3「必要な人の本籍」、選択(1つ)SEL1「謄本・抄本」(選択肢1:謄本 2:抄本)、1行入力TXT4「筆頭者氏名」、日付DT1「受領希望日」(和暦の日付入力)。各ブロック左端に並べ替え用のハンドル、右端に削除ボタンがある

DSLの6行が、そのままブロック6個になって積み上がる。並べ替えはハンドルをドラッグするだけ。

申請者が実際に開く決裁・申請画面は、こうなる。

決裁・申請画面。上部にタブが3つ「ワークフロー情報」「決裁・申請PDF」「決裁申請」。所管課セレクトは「市民生活課」、申請書類セレクトは「戸籍公用申請」を選択済み。申請者「企画課システム管理係 システム管理者」、申請日「令和8年9月11日」の下に、水色の帯で「決裁後の通知」として『決裁が完了すると所管(市民生活課市民係)の全員へ未読回覧で通知が届き、所管が処理を進めます。申請者本人へは、承認・却下の結果が未読回覧で届きます。』という説明文がある。その下に見出し「戸籍公用申請」があり、ビルダーで組んだ6項目がそのままフォームとして並ぶ: 事業名(複数行のテキストエリア)、補助事業名(1行入力)、必要な人の本籍(1行入力)、謄本・抄本(ラジオボタン「謄本」「抄本」)、筆頭者氏名(1行入力)、受領希望日(日付入力、初期値「令和8年9月11日」)

ビルダーで組んだ6項目が、そのままDSLを経由して決裁・申請の入力フォームになる。人が見るのはここだけで、裏の6行は意識しない。

試験で自分がデータを壊した

機能が動くところまで組んだあと、試験用のテンプレートをコマンドラインから流し込んで更新していた。そこで自分のデータを自分で壊した。

mysql -N で項目定義を取り出すと、改行は実際の改行ではなく \n という2文字の文字列になって出てくる。それに気づかないまま curl --data-urlencode でそのままサーバへ投げてテンプレートを上書きしたら、項目の区切りが文字列の \n に化けて保存された。エラーは何も出ない。申請画面を開くと、項目がごっそり消えていた。

REPLACE(定義, '\\n', '<br>') で復元して事なきを得たが、原因を見ながら「見る」と「書く」を同じコマンドの並びでやったのがまずかったと分かった。mysql -N で見た結果をそのまま書く側の入力に使い回したのが間違いで、試験は見る経路と書く経路を分けるべきだった。

アップローダは「フォルダの中のindex.php」に投げている

所管から申請者へ書類を返す機能も、同じ理由でもう一段つまずいた。添付のアップローダ(blueimp)は、アップロード先URLとして保存先フォルダそのものを指定する作りになっている。決裁者用のフォルダは、決裁画面を開いたときに WorkflowAttcDocs/TEMP/ の中身(index.php とファイル置き場)を再帰コピーして初めて出来上がる。

所管用の返信タブを先に画面へ出しただけで、フォルダを作る処理を呼び忘れた。結果、アップロードしたファイルはブラウザ側のキューに並んだまま、何も起きない。ここもエラーが出ない。タブを表示する前に、決裁者用フォルダを作っているのと同じ関数(fncRecursiveCopyDir())を呼ぶようにして直した。「フォルダの雛形はディレクトリごとに用意する」という、このアプリでは前にも踏んだやり方を、今回も踏んでいた。

決裁で終わらせず、返信まで乗せる

ここまでは申請と決裁の話だったが、実際に動かしてみて、もうひとつ気づいた。NGWの決裁ワークフローにそのまま乗せると、紙を1枚も挟まない申請と返信が組める。

流れはこうなる。決裁が完了すると所管へ通知が届く。所管は、その通知への返信として書類を添付して送り返す。申請者は、届いた返信を確認してダウンロードする。ここまで、ずっとペーパーレスのままで済む。

決裁完了通知のモーダル画面。投稿者「建設水道課土木係 小野 大和(電話:4345)」、件名「【決裁申請 承認完了】【決裁・申請】建設水道課戸籍公用申請」。水色の帯に所管(市民生活課市民係)への連携通知本文があり、その下にコメント入力欄と「既読・返信」「回覧保存」「閉じる」ボタンが並ぶ。タブは「決裁文書」「返信書類」「所管から返信」の3つ。画面下半分は左右2枚のPDFビューアで、左に決裁印が押された決裁書類、右に添付された住宅の全景写真(一部ぼかし済み)が表示されている

所管に届く通知には、決裁文書と添付書類のプレビューに並んで「所管から返信」タブがある。

所管から返信タブを開いた画面。灰色のパネルに「Add files...」ボタンと、アップロード済みファイル「dummy1_54.pdf(44.43 KB)」の行、コメント欄には「公用の戸籍謄本を送ります」と入力されている。右側にはアップロードしたPDFのプレビューが表示されている

タブの中身は、決裁者用フォルダで使っているのと同じ添付アップローダ。既存の部品をそのまま流用した。

通知一覧画面。上部にタブ「未読」「決裁通知 NEW」「保存回覧」「期限付」「既読」、右に「直近30日 最新100件」の表示。一覧の1行目が緑色の背景で「2026-09-11 佐藤七海 Re:【決裁申請 承認完了】【決裁・申請】建設水道課戸籍公用申請」となっており、件名の先頭に返信を表す「Re:」が付いている。2行目以降は通常の白背景の行が並ぶ

返信が付くと件名の先頭に「Re:」が付き、決裁・申請がらみの通知だけ行が緑色になって並びの中でも目立つ。

申請者側に届いた返信通知の画面。投稿者「市民生活課市民係 佐藤七海(電話:2563)」、件名「Re:【決裁申請 承認完了】【決裁・申請】建設水道課戸籍公用申請」。水色の帯に「所管からの返信書類があります。「返信書類」タブでご確認ください。」という案内文と、「佐藤 七海:wrote 公用の戸籍謄本を送ります」というコメントの引用が表示されている

申請者に届くのはここまで。あとは「返信書類」タブを開いて、送られてきたファイルを確認・ダウンロードするだけ。

結局、新しく作ったのは「所管から返信」タブと、決裁完了時に飛ばす通知の宛先設定くらいだった。既存のワークフロー(通知・回覧・添付アップローダ)の仕組みに少し手を入れただけで、申請から返信までのやり取りが紙を挟まずに一周した。

休暇申請も、同じビルダーに乗せた

決裁申請設定に手を入れたついでに、原型だった休暇マスターの画面も同じビルダーに乗せ替えた。休暇と決裁申請ではDSLの流儀が少し違う(休暇には高さの枠が無い、選択肢の番号が別の位置にある)ので、ビルダーに「プロファイル」という概念を持たせ、休暇用と決裁申請用の違いを吸収した。休暇の既存DSLも、ビルダーに通して書き出し直しても1文字も変わらないことを確認してから切り替えた。

もともと1つの画面のために作ったビルダーが、原型だった側にも被せ直せたのは、保存形式のDSLを最後まで変えなかったからだった。

数字で見る

項目値
復活に要した本質的な変更セレクトのコメントアウト解除(1箇所)
追加したライブラリ0(既存の jQuery UI 1.11 sortable/draggable を流用)
DSLの種別11種類(注記・1行・複数行・日付・日時・期間・時刻・時間帯・選択1つ・選択複数・その他)
既存DSLをビルダーに往復させた差分0(決裁申請・休暇マスターとも)
自由記述のまま残した既存データ27,117件

AIとの協働作業での学び

「なぜ使われなかったか」より「今どこまで出来ているか」を先に聞いた。 誰も使っていない機能だと思い込んでいたので、最初から作り直すつもりで掘り始めた。実際にはマスターCRUDからPDF出力まで大半が実装済みで、要らなかったのは「もう一度全部作る」ではなく「止まっている1箇所を探す」だった。作り直す前に現状を洗い出す聞き方をしたから、無駄な実装をせずに済んだ。

索引のズレは、人に何番目かを数えさせている限り再発する。 @ 区切りの何番目が高さで何番目が選択肢か、画面・PDF・ヘルプがそれぞれ違う番号を信じていた。1箇所直しても、また誰かが違う番号で書けば同じ不具合が起きる。ビルダーを被せたのは機能の見た目を良くするためではなく、人に数えさせる場面そのものを無くすための選択だった。

「見る」コマンドの出力を、そのまま「書く」コマンドに使い回さない。 mysql -N の出力にある \n が文字列だと知らずに curl へ流し、自分の試験データを自分で壊した。AIに試験手順を組んでもらうときも、読み取った値をそのまま書き込みに使う経路は、間に1段変換や確認を挟むよう頼むべきだった。楽をしようとした場所で壊した。