# LINEWORKS Form → プリザンター連携 設計書 日付: 2026-08-24(全面改訂) ## 背景・目的 LINEWORKS Form機能で収集したアンケート回答、プリザンターのテーブルへ連携。対象フォーム数、今後増える想定(部署ごとに追加等)。 初版設計(app-portal経由の単純な今すぐ実行ボタン、フォーム設定は固定コード管理)から、以下の運用要件が追加され全面改訂した: - 使用する連携キー(LINEWORKS User Account認証情報)を、手動実行時にフォームごとに選択・記憶し、以降の自動実行はその記憶した連携キーを使う - 一度も手動実行していないフォームは自動実行もスキップする(安全装置) - 自動実行の周期もフォームごとに手動実行画面から設定できる - フォームの質問→プリザンター列マッピングも、コードでなくプリザンター側で管理する(フォームごとに項目数・種類がまばらなため) ## 制約(調査結果) - Form API呼び出し、**User Account OAuth(Authorization Code)必須**。Service Account/Bot(JWT)不可。scope: `form`, `form.read`。 - Webhook/イベント通知**なし**。ポーリング必須。カーソルベースページング(`cursor`, `count`最大1000)のみ、期間指定・差分取得パラメータ**なし**。 - `GET /forms/{formId}/responses`、フォーム**作成者または共同管理者**が呼べる。作成者本人限定でない。 → 連携専用LINEWORKSアカウントを対象フォームへ「共同管理者」として追加する運用で対応。 - **n8nのData Table機能、状態永続化用途で恒久使用禁止**(ハングアップ元凶、[[feedback_n8n_datatable_prohibition]])。→ n8nは状態を一切持たない設計とする。 - **User Account OAuth認証部分は共通化**。独立サービス[[project_lineworks_user_auth]](`apps/lineworks-user-auth`)が認証UI・トークン管理・自動リフレッシュを一元提供。本アプリは`GET /token?key=<連携キー>`を叩くだけ。 - 実データ確認済み(2026-08-24、`kenichiro.nogi`連携キーで実機検証)。`GET /forms/{formId}/responses`のレスポンス構造: - 各質問に固定`questionId`(16進文字列)が付く → マッピングの識別子として使える - `respondent.email`が回答に含まれる → 照合キーとして使える - `answers`は常に配列(単一選択でも1要素、複数選択は複数要素、未回答は空配列) - 確認済み`questionType`: `SINGLE_CHOICE` / `MULTI_CHOICE` / `TEXT`(公式ドキュメント上は10種類、他タイプは未確認) - LINEWORKS Refresh Tokenの有効期限は**90日**(公式ドキュメント確認済み、[[project_lineworks_user_auth]]参照)。自動実行の周期は、90日以内に最低1回リフレッシュが発生する前提で設計する。 ## 全体アーキテクチャ ``` [人] --ブラウザ--> [Node.js管理画面(lineworks-form-sync、/manage/*)] ├─ フォーム一覧・連携キー/周期設定(lineworks-user-authのGET /keysを参照) ├─「今すぐ実行」→その場で処理実行、設定保存、手動実行済みフラグON └─「テスト回答取得」→質問マッピング雛形を自動生成・保存 [n8n Scheduleトリガー(固定頻度、例:15分ごと)] --> POST /execute (専用実行キー) → 対象フォームをループ、各フォームの周期・手動実行済みフラグを見て 実行すべきものだけ処理(自動/手動で共通の実処理ロジックを使う) [app-portal] --web型「開く」ボタン--> 管理画面URLへ遷移(単なる入口、任意) [Node.js /execute 処理本体] │ GET /token?key=<フォームごとに選択済みの連携キー> → [lineworks-user-auth] │ ├→ LINEWORKS Form API(回答一覧・cursor) ├→ LINEWORKS添付ファイルAPI(同トークン) └→ プリザンター Items API(respondent.emailで検索→更新 or 新規) ``` ### 役割分担 | コンポーネント | 役割 | |---|---| | n8n | Scheduleトリガーのみ(固定頻度)。`/execute`を叩くだけ、状態は一切持たない | | Node.js新規アプリ(`apps/lineworks-form-sync`) | 管理画面(手動実行・設定UI)+実処理本体(自動/手動共通ロジック)。全ての設定・状態はプリザンター側で管理 | | lineworks-user-auth([[project_lineworks_user_auth]]) | OAuth認証UI・トークン管理・自動リフレッシュ。今回`GET /keys`(連携キー一覧API)を追加実装 | | app-portal | 管理画面への入口リンクのみ(web型「開く」ボタン、任意) | ## データ管理(プリザンター側へ全て集約、n8n Data Table不使用) ### 1. フォーム管理テーブル(新規、1行=1フォーム) | 列 | 内容 | 設定者 | |---|---|---| | FormId | LINEWORKS Form ID | 人(先に手動入力) | | 回答格納先SiteId | 保存先プリザンターテーブルのSiteId | 人(先に手動入力) | | ユーザー照合列 | 保存先テーブルのうち、回答者メールアドレスで突合する列の物理列名 | 人(手動入力) | | 連携キー | lineworks-user-authの連携キー名 | 管理画面(手動実行時にプルダウン選択・記憶) | | 周期 | 15分/30分/1時間/3時間/6時間/1日 | 管理画面(手動実行時にプルダウン選択・記憶) | | 質問マッピング | JSON配列(下記形式)、Description列に格納 | 管理画面「テスト回答取得」で雛形生成→人がtargetColumnを追記 | | 手動実行済みフラグ | 未実行なら自動実行させない安全装置 | 管理画面「今すぐ実行」実行時にON | | 最終手動実行日時 / 最終自動実行日時 | 監視用 | システムが更新 | ### 質問マッピングJSON形式(実データ確認済み、2026-08-24) ```json [ { "questionId": "4642ccbd415f", "questionType": "SINGLE_CHOICE", "title": "節目検診・人間ドック希望の方", "targetColumn": "Class001" }, { "questionId": "dfebd6143c6a", "questionType": "MULTI_CHOICE", "title": "オプションをご希望の方(複数選択可)", "targetColumn": "Class003" } ] ``` `questionId`/`questionType`/`title`はLINEWORKS APIレスポンスの`questions`配列からそのまま継承。`description`(質問の長文説明)・`required`は含めない(マッピング判断に`title`で十分、不要な長文を避ける)。`targetColumn`のみ本アプリで追加する4つ目のキー。 ### 2. 回答格納先テーブル(フォームごと、既存方針通り) ユーザー照合列(メールアドレス)を含む。質問マッピングの`targetColumn`で指定された列群へ回答を格納。 ### 3. LINEWORKS連携トークンテーブル([[project_lineworks_user_auth]]管理、既存) 本アプリは直接触れない。`lineworks-user-auth`経由でのみアクセス。 ## マッピング雛形自動生成機能(新規) 管理画面の「テスト回答取得」ボタン: 1. 対象フォームに選択済みの連携キー(未選択ならエラー、先に連携キー選択を促す)でaccess token取得 2. `GET /forms/{formId}/responses`(count=1程度)を呼び、最新1件の回答を取得。0件ならエラー表示 3. 取得した`questions`配列から`questionId`/`questionType`/`title`を抽出 4. 既存の質問マッピングとマージ: 既存`questionId`は`targetColumn`を保持、新規`questionId`のみ`targetColumn`を空文字列で追加 5. フォーム管理テーブルの質問マッピング列へ保存 これにより、質問マッピングの骨格(質問ID・タイプ・タイトル)は自動取得され、人は`targetColumn`(プリザンター列名)をプリザンター側UIで追記するだけで済む。フォームに質問が追加された場合も、再度「テスト回答取得」を実行すれば新規質問分だけ追加され、既存の対応関係は保持される。 ## 投入ロジック(Upsert方式) `/execute`(自動実行、n8n Scheduleから)実行時、対象フォームごとに: 1. 手動実行済みフラグ確認 → falseならスキップ(未手動実行のフォームは自動実行しない) 2. 前回実行日時 + 設定周期から実行時刻判定 → 未到来ならスキップ 3. 連携キーのaccess token取得(`lineworks-user-auth`の`GET /token`経由) 4. `GET /forms/{formId}/responses`呼び出し(cursorページング) 5. 各回答について、`respondent.email`で回答格納先テーブルを検索 - 一致するレコードあり → 質問マッピングに従って該当列を更新 - 一致なし → ユーザー照合列にメールアドレスをセットしつつ、質問マッピングに従って新規作成 6. 添付ファイルがある質問は、LINEWORKS添付ファイルAPI(同access token)でダウンロード→プリザンターItems APIの添付欄へアップロード 7. 最終自動実行日時を更新 **手動実行**(管理画面「今すぐ実行」ボタン)は、上記1・2を省略して即実行。実行後に選択した連携キー・周期をフォーム管理テーブルへ保存し、手動実行済みフラグをON、最終手動実行日時を更新する。 ### questionType別 格納ルール(全10種類、実データ確認済み・2026-08-24) サンプルフォーム(`055dd7b8bbd336b1bbfd8fca982e3bb9`)で全10種類のquestionTypeを実データ確認した。 | questionType | 実際のanswers例 | 格納ルール | |---|---|---| | SINGLE_CHOICE | `["A"]` | `answers[0]`をClass/Description型列へ | | MULTI_CHOICE | `["B","A"]` | `answers.join(',')`をClass/Description型列へ | | DROPDOWN | `["A"]` | `answers[0]`をClass/Description型列へ | | TEXT | `["あ\nあああ"]` | `answers[0]`をClass/Description型列へ(改行含みうる) | | RATING | `["3"]` | `answers[0]`をClass/Description型列へ(数値文字列のまま) | | SINGLE_DATE(日程投票・単一) | `["2026-08-24T00:00:00+09:00"]` | `answers[0]`をClass/Description型列へ(Date型列非対応、範囲形式になりうるため) | | MULTIPLE_DATE(日程投票・複数) | `["2026-08-25T00:00:00+09:00","2026-08-24T00:00:00+09:00"]` | `answers.join(',')`をClass/Description型列へ(Date型列非対応) | | DATE_INPUT | `["2026-08-12T00:00:00+09:00"]` | targetColumnがDate型列なら`answers[0]`をそのまま渡す。**公式ドキュメントは「YYYY-MM-DD」形式と説明しているが、実データはDATETIME_INPUTと同じISO 8601形式(`T00:00:00+09:00`固定)で返る。ドキュメントでなく実データを正とする** | | DATETIME_INPUT | `["2026-08-24T02:30:00+09:00"]` | targetColumnがDate型列なら`answers[0]`をそのまま渡す | | ATTACHMENT | `["486838983"]`(attachmentId) | `GET /forms/{formId}/responses/{responseId}/attachments/{attachmentId}`でダウンロード→プリザンター添付欄へアップロード。テキスト列への格納は行わない | **targetColumnの型判定**: 物理列名のプレフィックス(`Class`/`Description`/`Date`)から判定する(`pleasanterClient.js`の`toHashPayload`と同じprefix規則)。`DATE_INPUT`/`DATETIME_INPUT`以外の質問タイプで`targetColumn`に`Date*`列が指定された場合はエラーとする(範囲・複数候補を保持できないため)。 ## エラーハンドリング・通知 `/execute`、失敗時に適切なHTTPステータス返却。n8n側でエラー分岐→SMTP通知(既存`n8n-wf-admin@next-hd.co.jp`)。 ## 認証 - **管理画面**(`/manage/*`): [[project_lineworks_user_auth]]の`adminAuth.js`/`adminView.js`を完全踏襲(Master Key + セッションCookie + 60分限定の一時アクセスキー発行機能、確定)。 - **`/execute`**(n8n→Node.js): 専用実行キー - **`lineworks-user-auth`の`GET /keys`・`GET /token`**: 本アプリ専用の実行キー(消費側ごとに別キー、既存方針[[project_org_master_sync_webhook_auth_keys]]踏襲) ## lineworks-user-auth側への追加作業(本プロジェクトの一部として実施) `GET /keys`(登録済み連携キー一覧を返すJSON API)を新規実装。管理画面の連携キー選択プルダウンがこれを参照する。実行キー認証で保護(または管理セッション保護、実装計画で確定)。 ## Node.jsアプリのエンドポイント一覧(暫定) | パス | 呼び出し元 | 認証 | 役割 | |---|---|---|---| | `/health` | dokploy-webapp規約 | なし | ヘルスチェック(必須) | | `/execute` | n8n | 専用実行キー | 対象フォームをループし、周期・フラグ判定→実処理(自動実行) | | `/manage/*` | 人(ブラウザ) | Master Key + セッション(一時キー可) | 手動実行ウィンドウ(フォーム一覧・連携キー/周期設定・今すぐ実行・テスト回答取得) | app-portalは`/manage`(または管理画面トップ)への「開く」リンクとして登録(web型、`PORTAL_APP_TYPE=web`)。中継エンドポイントは不要(旧設計の`/portal-trigger`は廃止)。 ## n8n Scheduleトリガー頻度(確定) 15分ごと(固定)。全フォーム対象の最短周期選択肢(15分)に合わせる。実際の実行要否はNode.js側でフォームごとの周期・フラグを見て判定する。 ## 未確定・今後の確認事項 - LINEWORKSユーザーマスタとの対応(ユーザー識別ID同士の紐付け)は別プロジェクト(LINEWORKSユーザーマスタ連携テーブル、計画中)で対応予定。本アプリは回答者メールアドレスでの直接突合のみ行い、マスタ連携には関与しない - 対象フォームの具体的な一覧・作成部署(実装計画時に確定)