メニューを、データで管理するアプリメニューと NGWT_APP_MENU の JSON
NGWはアプリごとに専用のメニュー体系を出す。40超のアプリに40通りのメニュー。それをHTMLにベタ書きせず、NGWT_APP_MENU テーブルの1列にJSON配列として持つ。起動可能アプリは職員区分ごとに NGWT_BOOT_MANAGER で管理し、メニューで隠し起動で防ぐ。データで持つから、並べ替えも権限の出し分けも自治体ごとの差し替えも、デプロイなしでできる。
アプリの数だけ、メニューがある
NGWは、いま開いているアプリごとに、そのアプリ専用のメニューを画面上部に出す。収受文書を開けば「収受文書を開く/印刷/文書管理カート/添付書類結合…」が並び、会議録を開けば別の顔ぶれになる。文書管理システムだけでも登録アプリは40を超える。つまり、アプリの数だけメニュー体系がある。

これを画面ごとにHTMLでベタ書きしていたら、どうなるか。アプリをひとつ足すたびにメニューのHTMLを書き、並べ替えのたびにHTMLをいじり、「このボタンは管理者だけ」「この自治体では名前が違う」が全部コードに散らばる。40アプリぶん、それが積み上がる。
NGWは、メニューをHTMLではなくデータとして持つことで、これを避けている。
メニューは、JSONで持つ
置き場所は NGWT_APP_MENU テーブル。1アプリ = 1行だ。行にはアプリ名・起動PHP・アイコン・並び順(ORDER_NO)・有効フラグ(ENABLE)などが入る。そして肝心のメニュー項目は、その行の NGWT_APP_MENU_EVENT_JSON 列に、JSON配列として畳まれている。
収受文書アプリなら、中身はおおよそこうだ。
[
{ "NAME": "収受文書を開く", "MODAL": "MENU", "ACTIVE_EVENT": "ALWAYS",
"FUNC": "InitListColumnDocuBunsho(frmNgw)", "ICON": "far fa-folder-open" },
{ "NAME": "印刷", "MODAL": "MENU", "ACTIVE_EVENT": "ALWAYS",
"FUNC": "", "ICON": "fal fa-print" },
{ "NAME": "文書管理カート", "MODAL": "MENU",
"ACTIVE_EVENT": "SELECT+,SELECT_NOT_UPDATE+",
"FUNC": "ajaxNgwFunc_MenuJumpBlank(frmNgw, '/ngw/docu/...')",
"ICON": "fal fa-credit-card" }
]
配列の1要素が、メニューの1項目。キーはこう読む。
NAME… 表示名。「簿冊」「棚」のような自治体ごとの呼称は、表示するときに差し替える(後述)。ICON… アイコン。Font Awesome のクラス(far fa-folder-openなど)をそのまま持つ。FUNC… クリックで動く処理。そのアプリ内の関数(InitListColumnDocuBunsho(frmNgw))を呼ぶか、ajaxNgwFunc_MenuJump(...)で別画面へ飛ぶか。メニューの「行き先」がこの一文字列で決まる。MODAL… 項目の種別。MENU(ふつうのボタン)/SPACER(間隔)/DROPDOWN(ドロップダウン)/LAYERMENU(階層メニュー)/COLORBOX_MODAL(モーダルで開く)/ADMIN_MENU(管理者だけ)…。キー名は経緯からMODALのままだが、いま入っているのは表示の型だ——こういう化石の名前が残るのも、10年動くシステムの正直なところである。ACTIVE_EVENT… このボタンが押せる(active)になる画面状態。ALWAYS(常時)/SELECT+(一覧で行を選んだとき)/SELECT_NOT_UPDATE+(未更新の行を選んだとき)…をカンマで並べる。「どの状態でどのボタンが効くか」を、コードでなくデータで宣言する。
サーバで組み、器に流す
描画は、アプリの仕組みで書いたSubModeの型そのままだ。
クライアントの ajaxNgwFunc_MakeAppMenu が、&SubMode=AppMenu を付けてPOSTする。サーバの ajaxNgwFunc.php はそれを受け、fncMakeAppMenu が NGWT_APP_MENU の行を引き、JSONを
$menuArray = json_decode($valNgwtAppMenuEventJson, true);
foreach ($menuArray as $menu) { /* <li>ボタンを組み立てる */ }
とほどいて、<li> のボタン列を組み上げて返す。返ってきたHTMLは、クライアントが $("#AppMenu").html(result) で流し込む。
——この #AppMenu、前回の個人設定の記事で見た画面PHPに、<ul id="AppMenu" class="app-menu"></ul> という空っぽの器として置いてあったものだ。どのアプリ画面にも同じ空の棚があり、そこへ、そのアプリ用のメニューがJSONから組まれて載る。
データだから、できること
メニューをデータに追い出すと、こういうことが「デプロイなし」でできるようになる。
- 増減・並べ替え・改名 … アプリを足す、ボタンを1つ増やす、順番を変える、名前を直す——行とJSONを編集するだけ。PHPを触らない。
- 権限での出し分け … メニュー生成のSQLは
FIND_IN_SET(NGWT_APP_MENU_ID, '$BootAppCsv')で、その職員が起動できるアプリだけに絞る。見せてよいものだけがメニューに出る。可視性を権限で決めるのは、プライバシー保護の記事と同じ発想だ。 - 状態での活性制御 … 「行を選ぶまで“カートに入れる”は押せない」を、
ACTIVE_EVENTに書いておくだけで効く。 - 用語の方言 … 「簿冊」「棚」といった呼称を、その自治体の言い方へ表示時に差し替える。中身のコードは共通のまま、見え方だけ地域に合わせる。
- 自治体スペシャル … 同じメニュー項目でも、自治体番号付きの専用PHP(
docuKessaiKeiro47000.phpのような)が存在すれば、file_existsで見つけてそちらを起動する。基本は全自治体で共通、必要な自治体だけそっと差し替え——前回の「みんなの既定+ひとりの上書き」と、まったく同じ形だ。
そのCSVは、誰が決めるのか — 起動管理
さきほどさらっと出てきた BootAppCsv。あれは「その職員が起動してよいアプリのID一覧(CSV)」だが、では誰がそれを決めるのか。担っているのが ngwtBootManager.php(APP起動管理)と、その裏の NGWT_BOOT_MANAGER テーブルだ。
このテーブルは職員区分(役職)ごとに1行を持つ。会計年度任用職員・職員・係長・課長…という区分それぞれに、「起動してよいアプリ」のチェックが並び、そのチェックの集合が BOOT_APP_CSV(NGWT_APP_MENU_ID のCSV)になる。

メニューを組むとき、NGWはこの区分の BOOT_APP_CSV を ngwFunc_GetBootAppCsv($ManagerPosition) で引き、FIND_IN_SET(NGWT_APP_MENU_ID, '$BootAppCsv') でメニューに出すアプリを絞る。だから、起動権のないアプリは、そもそもメニューに現れない。
ただし——隠すだけでは、守ったことにならない。URLを直打ちすれば画面に入れてしまうなら、非表示はただの気休めだ。そこでNGWは二段構えにしている。アプリ起動時にも ngwFunc_CheckBootableApp($ThisPHP, $ManagerPosition) が走り、その区分の BOOT_APP_CSV にそのアプリが含まれているかを確かめ、含まれなければ起動そのものを止める。メニューで隠し(見せない)、起動で防ぐ(入れない)。 プライバシー保護の記事で書いた「隠す/防ぐ」の二段構えが、アプリの入口でもそのまま効いている。
管理画面もまた、この仕組みで
そのJSONを人が編めるように、専用の管理アプリがある。

上半分がアプリの台帳(NGWT_APP_MENU の行)、下半分の「MENU ACTIVE SETTING」がJSONをグリッドで編集する部分だ。人はJSONの波括弧を直接書くのではなく、行を足し、プルダウンで種別を選び、FUNC と ICON を入れる。保存すると、それがJSON配列に組み直されて1列に収まる。
面白いのは、この管理画面の上部ツールバー(登録保存/更新保存/削除/一括更新…)それ自体も、同じアプリメニューの仕組みで描かれていることだ。メニューを管理する画面のメニューが、メニューの仕組みで動いている。
AIとの協働作業での学び
HTMLに埋め込んだ構造は「コード」になり、変えるたびにデプロイが要る。データに追い出した構造は「設定」になり、運用の中で変えられる。この線引きが、システムの寿命を左右する。
メニューは、その典型だ。頻繁に変わり、自治体ごとに違い、権限で出し分ける——コードに固めるには不安定すぎる。だからJSON1列に畳み、40を超えるアプリでコードはひとつ、変わるのはデータだけ、という形にした。「どこまでをコードにし、どこからをデータにするか」。地味だが、この判断こそが設計のいちばん太い背骨である。