開発情報・ナレッジ

投稿者: SPIRERS ナレッジ向上チーム 2022年8月31日 (水)

LINE会員アプリ サイト設計・構築

多くの方が利用されているLINEとSPIRAL ver.2を連携した、LINE会員アプリをご紹介します。
会員登録後はお問い合わせができ、自身のお問い合わせの履歴確認や自身宛の通知やお知らせの確認も可能です。
この記事は サイト設計・構築 のフェーズとなります。
LINE お問い合わせデモを確認
スマホ確認用 友だち追加QRコード
 デモを確認するためには、LINEの連携が必要となります。
 会員登録画面に遷移時にLINEより名前等の情報が引き込まれます。
 名前は管理画面から閲覧可能となっておりますので、個人が特定できないニックネーム等に変更をお願いします。
関連記事はこちら
SPIRAL ver.2はプログラミング経験がなくても、オリジナルの業務アプリの制作・カスタマイズができるローコード開発プラットフォームです。
詳しくは SPIRAL ver.2 とはをご覧ください。

サイト機能

SPIRAL ver.2 のサイト管理機能では、静的なWebページを作成したり、
データベースに対して登録・更新するフォーム や データベースの情報を一覧形式で表示させるページなどを作成できます。
詳細は、サイト機能の全体像を確認してください。
今回は、サイト機能を使って、「LINE会員アプリ」を作成します。

注意

設定の説明部分にて、ソースコードを記載しております。
ソースコードに記載されている ブロックのID や 各種リンク や name値 含めて文言などは、デモの環境に合わせたものとなります。
実際に同様のものを作成する際は、変更が必要となる箇所がありますので、ご注意ください。

フロー・使用機能の整理

LINE会員アプリの全体フローを整理し、認証エリア内で触れる画面と使用している機能をまとめました。
非会員のフロー図
会員のフロー図
▼使用する機能
認証エリア LINEアカウントでの認証エリア
ページ機能 会員登録画面などの各種ページにて使用
ブロック機能 登録フォーム
お問い合わせ登録で使用しています
ブロック機能 更新フォーム
会員登録フォーム / 会員登録情報変更フォーム で使用しています
ブロック機能 削除フォーム
会員退会フォームで使用しています
ブロック機能 レコードリスト
自身の質問お問い合わせ などで使用しています
ブロック機能 レコードアイテム
自身の質問お問い合わせ詳細 などで使用しています
ブロック機能 フリーコンテンツ
ヘッダ・フッタ で使用しています
全体のフォルダ構成
サイト側では Line ログイン処理およびリッチメニューの切り替え処理をいれています。
下記を使用して実装しておりますので、詳細等は下記を参照ください。

各機能の詳細に関しては、下記を参照ください。
ページ機能
ブロック機能
認証エリア

LINEの設定およびログイン処理

手順1:LINEのリッチメニュー作成
LINE Messaging APIでのリッチメニュー作成、リッチメニューID取得 を参考にリッチメニューを作成します。
非会員用のデフォルトのリッチメニューの作成および反映 と 会員用のリッチメニューを作成を行います。
会員用のリッチメニューIDは、登録処理のリッチメニュー切り替えプログラムで使用します。
手順2:LINEログイン用の認証エリアの作成
LINEログイン用の認証エリアを作成するため、認証エリアの「+」より作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
手順3:LINEログインの処理の作成
ログイン画面にLINEアカウントで認証エリアにログインさせるサンプルプログラムを参考にLINEログインの処理いれます。

PHP
<?php
// SPIRAL ver.2 の情報
define("LOGIN_URL","https://pb-sp2demo-application2.spiral-site.com/lineContact/login");
define("LOGIN_TOP_PASS","/lineContact");
define("ENCRYPT_KEY",""); // 文字列
define("API_URL", "https://api.spiral-platform.com/v1/");
define("API_KEY", $SPIRAL->getEnvValue("API_KEY"));
define("API_ROLE", "");
define("SITE_ID", "");
define("AUTHENTICATION_ID", "");
define("APP_ID", $SPIRAL->getEnvValue("APP_LINE_CONTACT_ID"));
define("USERDB_ID", $SPIRAL->getEnvValue("DB_LINE_USER_ID"));
// Line の情報
define("CLIENT_ID", "");
define("CLIENT_SECRET", "");


// セッションスタート
session_start();
// ステータスをセット
// Redirect用URL
if(!$SPIRAL->getParam("back-url")){
    $redirect_uri = LOGIN_URL;
}else{
    $redirect_uri = LOGIN_URL.'?back-url='.$SPIRAL->getParam("back-url");
}
$SPIRAL->setTHValue("status","");
if(!$SPIRAL->getParam("code")){
    $_SESSION['randStr'] = makeRandStr(8);
    // Lineに未ログイン状態
    $SPIRAL->setTHValue("status","pageRedirect");
    
    $SPIRAL->setTHValue('redirectURL','https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id='.CLIENT_ID.'&redirect_uri='. $redirect_uri .'&state='. $_SESSION['randStr'] . '&bot_prompt=aggressive&scope=profile%20openid%20email');
}else{    
    if($SPIRAL->getParam("state") == $_SESSION['randStr']){
        $_SESSION['randStr'] = null;
        $lineData = lineLogin($SPIRAL->getParam("code"),$redirect_uri);
        if(!isset($lineData['sub'])){
            $SPIRAL->setTHValue("status","error");
            $SPIRAL->setTHValue("errorMessage","Line の認証に失敗しました。");
            // debug
            //$SPIRAL->setTHValue("errorMessage","Line認証".print_r($lineData,true));
        }else{
            $data = array(
                "LineID" => $lineData['sub'],
                "password" => encrypt($lineData['sub']),
                // 名前を使用する場合
                "name" => $lineData['name'],
            );
            if(isset($lineData['picture'])){
                // プロフィールアイコンを入れる場合
                $data = array_merge($data,array("image" => $lineData['picture']));
            }
            if(isset($lineData['email'])){
                $data = array_merge($data,array("mail" => $lineData['email']));
            }            
            $regData = apiCurlAction("POST","apps/".APP_ID."/dbs/".USERDB_ID."/records",$data);
            $loginUser = array(
                "id" => $lineData['sub'],
                "password" => encrypt($lineData['sub']),
            );
            $loginCheckAPI = apiCurlAction("POST","sites/".SITE_ID."/authentications/".AUTHENTICATION_ID."/login",$loginUser);
            if(isset($loginCheckAPI["status"]) && $loginCheckAPI["status"] != 200){
                // DB情報取得に失敗
                $SPIRAL->setTHValue("status","error");
                $SPIRAL->setTHValue("errorMessage","ログイン処理のAPIの処理に失敗しました。時間をおいて再度お試しください。");
                // debug
                $SPIRAL->setTHValue("errorMessage","ログイン処理".print_r($loginCheckAPI,true));
            }else{
                if($SPIRAL->getParam("back-url")){
                    if(strpos($SPIRAL->getParam("back-url"),'?') !== false){
                        $parameter = str_replace('?', '', strstr($SPIRAL->getParam("back-url"), "?"));
                        $path = strstr($SPIRAL->getParam("back-url"), "?", true);
                    }else{
                        $path = $SPIRAL->getParam("back-url");
                    }
                }else{
                    $path = LOGIN_TOP_PASS;
                }
                $loginData = array(
                    "token" => $loginCheckAPI['token'],
                    "path" => $path,
                );
                $_SESSION['token'] = $loginCheckAPI['token'];
                $loginAPI = apiCurlAction("POST","sites/".SITE_ID."/authentications/".AUTHENTICATION_ID."/oneTimeLogin",$loginData);
                if(isset($loginAPI['status']) && $loginAPI["status"] != 200){
                    // DB情報取得に失敗
                    $SPIRAL->setTHValue("status","error");
                    $SPIRAL->setTHValue("errorMessage","APIの処理に失敗しました。時間をおいて再度お試しください。");
                    // debug
                    //$SPIRAL->setTHValue("errorMessage","リダイレクト処理".print_r($loginAPI,true));
                }else{
                    $SPIRAL->setTHValue("status","pageRedirect");
                    if(isset($parameter)){
                        $SPIRAL->setTHValue('redirectURL',$loginAPI['url'].'&'.$parameter);
                    }else{
                        $SPIRAL->setTHValue('redirectURL',$loginAPI['url']);
                    }
                }
            }   
        }
    }else{
        $SPIRAL->setTHValue("status","error");
        $SPIRAL->setTHValue("errorMessage","不正なアクセスです。");
    }   
}



function lineLogin($code,$redirect_uri){
    $postData = array(
        'grant_type'    => 'authorization_code',
        'code'          => $code,
        'redirect_uri'  => $redirect_uri,
        'client_id'     => CLIENT_ID,
        'client_secret' => CLIENT_SECRET,
    );
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
    curl_setopt($ch, CURLOPT_URL, 'https://api.line.me/oauth2/v2.1/token');
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);

        
    $json = json_decode($response);
    $accessToken = $json->access_token;
    $postData2 = array(
        'id_token'    => $json->id_token,
        'client_id'     => CLIENT_ID,
    );
    $ch2 = curl_init();
    //curl_setopt($ch2, CURLOPT_HTTPHEADER, array('Authorization: Bearer ' . $accessToken));
    curl_setopt($ch2, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
    curl_setopt($ch2, CURLOPT_URL, 'https://api.line.me/oauth2/v2.1/verify');
    curl_setopt($ch2, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch2, CURLOPT_POSTFIELDS, http_build_query($postData2));
    curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
    $LineProfile = curl_exec($ch2);
    curl_close($ch2);
    
    $json2 = json_decode($LineProfile);
    $userInfo= json_decode(json_encode($json2), true);
    return $userInfo;
}

function makeRandStr($length) {
    $str = array_merge(range('a', 'z'), range('0', '9'), range('A', 'Z'));
    $r_str = null;
    for ($i = 0; $i < $length; $i++) {
        $r_str .= $str[rand(0, count($str) - 1)];
    }
    return $r_str;
}

/**
 * V2用 API送信ロジック
 * @return Result
 */
function apiCurlAction($method, $addUrlPass, $data = null){    
    $header = array(
        "Authorization:Bearer " . API_KEY,
        "Content-Type:application/json",
        "X-Spiral-Api-Version: 1.1",
    );
    if(API_ROLE){
        $header = array_merge($header,array("X-Spiral-App-Role: ".API_ROLE));
    }

    // curl
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_URL, API_URL . $addUrlPass);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    if ($method == "POST") {
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
    }
    if ($method == "PATCH") {
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
    }
    if ($method == "DELETE") {
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
    }
    if ($method == "GET") {
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
    }
    $response = curl_exec($curl);
    if (curl_errno($curl)) echo curl_error($curl);
    curl_close($curl);
    return json_decode($response, true);
}

/**
 * 暗号化
 * @return text
 */

function encrypt($data){
    return openssl_encrypt(substr($data,5), 'AES-128-ECB', ENCRYPT_KEY);
}
HTML - head
<title th:text="${page.title}"></title>
<th:block th:if="${cp.result.value['status']} == 'pageRedirect'">
    <meta http-equiv="refresh" th:content="|0;URL=${cp.result.value['redirectURL']}|">
</th:block>
HTML - body
<div th:if="${cp.result.isSuccess}">
    <th:block th:if="${cp.result.value['status']} == 'error'">
        <p th:text="${cp.result.value['errorMessage']}"></p>
    </th:block>
</div>
<div th:if="${!cp.result.isSuccess}">
    <p th:text="${cp.result.errorMessage}">error message</p>
</div>
詳細な設定方法に関しては、LINEアカウントで認証エリアにログインさせるサンプルプログラムをご確認ください。
ポイント①
LINE連携
今回のスターターデモでは、LINEのリッチメニュー作成と自動ログインを実行しています。
IDとPWは使用せず、LINEと連携を行い、自動ログインを行っています。
LINEアカウントで認証エリアにログインさせるサンプルプログラム
LINE Messaging APIでのリッチメニュー作成、リッチメニューID取得

会員情報登録/変更/削除 と リッチメニュー切り替え機能

手順1:会員登録ページの作成
LINEの設定およびログイン処理で作成した認証エリアに会員登録ページの作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
手順2:会員登録(更新ブロック)の作成
手順1で作成したページの BODY から「+」ボタンを押下し、更新ブロックを作成します。
会員登録は、更新ブロック となります。
LINEログイン時に事前に LINE ID の登録を行います。
引き込んだIDと突合する形で、会員情報登録を行うため、更新フォームでの作成となります。

デザインに関しては、コピペCSS 「シンプルモダン」を導入しています。
更新フォームに関しましては、標準のみの利用となります。

HTML - body
<div class="sp-form-container">
  <p>
    <span style="font-size: 18pt;">会員情報登録</span>
  </p>
      <!--/* 名前(name) */-->
    <sp:input-field name="f0xx"></sp:input-field>
    <div class="sp-form-item sp-form-field">
      <div class="sp-form-label">
        <th:block th:text="${fields['f0xx'].label}">
          Label
        </th:block>
        <span class="sp-form-required" th:if="${fields['f0xx'].required}" th:text="${fields['f0xx'].requiredIndicator}">*</span>
      </div>
        <div class="sp-form-data">
        <input type="text" class="sp-form-control" th:name="${fields['f0xx'].name}" th:value="${#maps.containsKey(inputs, 'f0xx') ? inputs['f0xx'] : siteClient.record[xx]}">
        <span class="sp-form-noted" th:if="${fields['f0xx'].help != null}" th:text="${fields['f0xx'].help}">Help text</span>
        <span class="sp-form-error" th:if="${errors['f0xx'] != null}" th:text="${errors['f0xx'].message}">Error message</span>
      </div>
    </div>
    <!--/* メールアドレス(mail) */-->
    <sp:input-field name="f0xx"></sp:input-field>
    <div class="sp-form-item sp-form-field">
      <div class="sp-form-label">
        <th:block th:text="${fields['f0xx'].label}">
          Label
        </th:block>
        <span class="sp-form-required" th:if="${fields['f0xx'].required}" th:text="${fields['f0xx'].requiredIndicator}">*</span>
      </div>
        <div class="sp-form-data">
        <input type="email" class="sp-form-control" th:name="${fields['f0xx'].name}" th:value="${#maps.containsKey(inputs, 'f0xx') ? inputs['f0xx'] : siteClient.record[xx]}">
        <div th:if="${fields['f0xx'].collation}">
          <span class="sp-form-email-reenter" th:text="${fields['f0xx'].reenterLabel}">Re-enter to confirm</span>
          <input type="email" class="sp-form-control" th:name="${fields['f0xx:reenter'].name}" th:value="${#maps.containsKey(inputs, 'f0xx:reenter') ? inputs['f0xx:reenter'] : siteClient.record[xx]}">
        </div>
        <span class="sp-form-noted" th:if="${fields['f0xx'].help != null}" th:text="${fields['f0xx'].help}">Help text</span>
        <span class="sp-form-error" th:if="${errors['f0xx'] != null}" th:text="${errors['f0xx'].message}">Error message</span>
      </div>
    </div>
    <!--/* このLINEアカウントをどこで知りましたか?(question) */-->
    <sp:input-field name="f0xx"></sp:input-field>
    <div class="sp-form-item sp-form-field">
      <div class="sp-form-label">
        <th:block th:text="${fields['f0xx'].label}">
          Label
        </th:block>
        <span class="sp-form-required" th:if="${fields['f0xx'].required}" th:text="${fields['f0xx'].requiredIndicator}">*</span>
      </div>
      <div class="sp-form-data">
        <div class="sp-form-dropdown">
            <select class="sp-form-control" th:name="${fields['f0xx'].name}">
              <option value="" th:text="${fields['f0xx'].unselectedLabel}" th:selected="${inputs['f0xx'] == null}">Select option</option>
              <option th:each="option : ${fields['f0xx'].options}" th:value="${option.id}" th:text="${option.label}" th:selected="${inputs['f0xx'] == #strings.toString(option.id)}">Item</option>
            </select>
            <span class="sp-form-dropdown-icon"></span>
          </div>
        <span class="sp-form-noted" th:if="${fields['f0xx'].help != null}" th:text="${fields['f0xx'].help}">Help text</span>
        <span class="sp-form-error" th:if="${errors['f0xx'] != null}" th:text="${errors['f0xx'].message}">Error message</span>
      </div>
    </div>  
  <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>
CSS に関してはコピペCSS 「シンプルモダン」の流用のため割愛いたします。
手順3:ヘッダ・フッタ用ブロックを作成
手順1で作成したページの BODY から「+」ボタンを押下し、フリーコンテンツブロックで共通となるヘッダを作成します。
すぐに使える!ヘッダ&フッター テンプレートを元に作成しています。
ヘッダは、非会員用と会員用の2パターン用意しています。
ソースコードは、テンプレートのままなので、割愛しますので作成の際は、すぐに使える!ヘッダ&フッター テンプレートを確認お願いします。
手順4:会員登録ページの設定
手順2、3で作成したブロックを会員登録ページに埋め込みます。
ヘッダに関しては、非会員用のヘッダを埋め込んでください。
また、会員登録ページでは、会員情報未登録のステータスの人以外アクセスできないよう、ページにアクセスをいれています。

HTML - head
<title th:text="${page.title}"></title>
<th:block th:if="${siteClient.record[xx]?.id} ne 1">
    <meta http-equiv="Refresh" content="0; URL=https://pb-sp2demo-application2.spiral-site.com/lineContact/">
</th:block>
HTML - body
<th:block th:if="${siteClient.record[xx]?.id} eq 1">
<!-- ヘッダ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- ヘッダ -->
    <!-- コンテンツ -->
    <sp:block name="ufb0xx"></sp:block>
    <!-- コンテンツ -->
    <!-- フッタ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- フッタ -->
</th:block>
また、会員登録ページでは完了時にリッチメニュー切り替えるプログラムの設定もいたします。
リッチメニューを切り替える処理に関しては、LINE Messaging APIでのリッチメニュー作成、リッチメニューID取得を参考に作成しています。

PHP
<?php
// SPIRAL ver.2 の情報
define("LINE_RECORD_ID", ""); // LineIDのフィールドID
// Line の情報
define('LINE_MESSAGE_TOKEN', '');// チャネルアクセストークン
define('RICH_MENU_ID', ''); // 変更したいリッチメニューID

// 登録ブロックの場合
// $formComplete = $SPIRAL->getRegistrationForm(""); // 登録ブロックの識別名を登録
// 更新ブロックの場合
$formComplete = $SPIRAL->getUpdateForm(""); // 更新ブロックの識別名を登録
// 削除ブロックの場合
// $formComplete = $SPIRAL->getDeleteForm(""); // 削除ブロックの識別名を登録

// 以下修正不要
if($formComplete->isCompletedStep()){
    $result = lineRichMenuChange($SPIRAL->getAuthRecordByFieldId(LINE_RECORD_ID));
    // debug
    // $SPIRAL->setTHValue("result", print_r($result,true));
}

function lineRichMenuChange($lineID){
    $ch = curl_init();   
    $header = array(
        'Content-Type: application/json',
        'Authorization: Bearer '.LINE_MESSAGE_TOKEN,
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    curl_setopt($ch, CURLOPT_URL, 'https://api.line.me/v2/bot/user/'.$lineID.'/richmenu/'.RICH_MENU_ID);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);   
    
    $json = json_decode($response);
    $userInfo= json_decode(json_encode($json), true);
    return $userInfo;
}
手順5:会員情報変更ページの作成
LINEの設定およびログイン処理で作成した、認証エリアに情報変更会員登録ページの作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
手順6:会員情報変更(更新ブロック)の作成
会員情報変更の更新ブロックは認証エリア作成時に自動生成される更新ブロックを活用します。
デザインに関しては、コピペCSS 「シンプルモダン」を導入しています。
こちらの更新フォームも標準設定になるため割愛します。
手順7:会員情報変更ページの設定
手順2、3で作成したブロックを会員情報変更ページに埋め込みます。
ヘッダに関しては、会員用のヘッダを埋め込んでください。

会員情報変更ページでは、会員情報済みのステータスの人以外アクセスできないよう、ページにアクセスをいれています。
また、会員情報変更ページと退会ページがタブ上で表示されるようにデザインを入れています。

HTML - head
<title th:text="${page.title}"></title>
<th:block th:if="${siteClient.record[xx]?.id} ne 2">
    <meta http-equiv="Refresh" content="0; URL=https://pb-sp2demo-application2.spiral-site.com/lineContact/reg">
</th:block>
HTML - body
<th:block th:if="${siteClient.record[xx]?.id} eq 2">
    <!-- ヘッダ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- ヘッダ -->

    <!-- タブメニュー -->
    <div class="menuTab">
      <a href="/lineContact/update"><div class="tabItem ">会員情報変更</div></a>
      <div class="tabItem active">会員情報削除</div>
    </div>
    <!-- <p th:text="${cp.result.value['result']}"></p>
    <p th:text="${cp.result.value['result2']}"></p> -->
    <!-- コンテンツ -->
    <sp:block name="dfb0xx"></sp:block>
    <!-- コンテンツ -->

    <!-- フッタ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- フッタ -->
</th:block>
CSS
.menuTab{
  margin-left: auto;
  margin-right: auto;
  max-width: 960px;
  height: auto;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: flex-end;
}
.menuTab a{
  text-decoration: none;
}

.menuTab .tabItem{
  min-width: 100px;
    padding: 10px 10px;
    text-align: center;
    background: #505050;
    font-size: 18px;
    color: #fff;
    border-top: none;
    border-left: none;
    border-right: none;
    border-bottom: solid 3px #505050;
    border-top-left-radius: 8px;
    border-top-right-radius: 8px;
    width: 150px;
}

@media (max-width: 768px){
    .menuTab .tabItem{
        padding: 10px 20px;
        width: auto;
    }
}

.menuTab .tabItem.active{
  background: #fff;
  color: #505050;
  border-top: solid 3px #505050;
  border-left: solid 3px #505050;
  border-right: solid 3px #505050;
  border-bottom: none;
  font-weight: bold;
  font-size: 20px;
}
手順8:退会ページの作成
LINEの設定およびログイン処理で作成した、認証エリアに退会ページの作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
手順9:退会処理(削除ブロック)の作成
退会処理の削除ブロックは認証エリア作成時に自動生成される更新ブロックを活用します。
デザインに関しては、コピペCSS 「シンプルモダン」を導入しています。
こちらの削除フォームも標準設定になるため割愛します。
手順10:退会ページの設定
手順8、9で作成したブロックを会員情報変更ページに埋め込みます。
ヘッダに関しては、会員用のヘッダを埋め込んでください。

退会ページでは、会員情報済みのステータスの人以外アクセスできないよう、ページにアクセスをいれています。
会員情報変更ページと同様のタブデザインとなっていますが、退会と会員情報で選択されているタブを切り替えています。

HTML - head
<title th:text="${page.title}"></title>
<th:block th:if="${siteClient.record[xx]?.id} ne 2">
    <meta http-equiv="Refresh" content="0; URL=https://pb-sp2demo-application2.spiral-site.com/lineContact/reg">
</th:block>
HTML - body
<th:block th:if="${siteClient.record[xx]?.id} eq 2">
    <!-- ヘッダ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- ヘッダ -->

    <!-- タブメニュー -->
    <div class="menuTab">
      <div class="tabItem active">会員情報変更</div>
      <a href="/lineContact/delete"><div class="tabItem">会員情報削除</div></a>
    </div>

    <!-- コンテンツ -->
    <sp:block name="ufb0xx"></sp:block>
    <!-- コンテンツ -->

    <!-- フッタ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- フッタ -->
</th:block>
CSS
.menuTab{
  margin-left: auto;
  margin-right: auto;
  max-width: 960px;
  height: auto;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: flex-end;
}
.menuTab a{
  text-decoration: none;
}

.menuTab .tabItem{
  min-width: 100px;
    padding: 10px 10px;
    text-align: center;
    background: #505050;
    font-size: 18px;
    color: #fff;
    border-top: none;
    border-left: none;
    border-right: none;
    border-bottom: solid 3px #505050;
    border-top-left-radius: 8px;
    border-top-right-radius: 8px;
    width: 150px;
}

@media (max-width: 768px){
    .menuTab .tabItem{
        padding: 10px 20px;
        width: auto;
    }
}

.menuTab .tabItem.active{
  background: #fff;
  color: #505050;
  border-top: solid 3px #505050;
  border-left: solid 3px #505050;
  border-right: solid 3px #505050;
  border-bottom: none;
  font-weight: bold;
  font-size: 20px;
}
また、退会ページでも完了時にリッチメニュー切り替えるプログラムの設定もいたします。
リッチメニューを切り替える処理に関しては、LINE Messaging APIでのリッチメニュー作成、リッチメニューID取得を参考に作成しています。
削除フォームを利用しますので、APIによるログアウト処理も実装しています。

PHP
<?php
// SPIRAL ver.2 の情報
define("LINE_RECORD_ID", ""); // LineIDのフィールドID
// 認証ページ
define("API_URL", "https://api.spiral-platform.com/v1/");
define("API_KEY", $SPIRAL->getEnvValue("API_KEY"));
define("API_ROLE", "");
define("SITE_ID", "xx");
define("AUTHENTICATION_ID", "x");
// Line の情報
define('LINE_MESSAGE_TOKEN', '');// チャネルアクセストークン
define('RICH_MENU_ID', ''); // 変更したいリッチメニューID


// セッションスタート
session_start();

// 登録ブロックの場合
// $formComplete = $SPIRAL->getRegistrationForm(""); // 登録ブロックの識別名を登録
// 更新ブロックの場合
// $formComplete = $SPIRAL->getUpdateForm(""); // 更新ブロックの識別名を登録
// 削除ブロックの場合
$formComplete = $SPIRAL->getDeleteForm(""); // 削除ブロックの識別名を登録

// 以下修正不要
if($formComplete->isCompletedStep()){
    $result = lineRichMenuChange($SPIRAL->getAuthRecordByFieldId(LINE_RECORD_ID));
    // debug
    // $SPIRAL->setTHValue("result", print_r($result,true));
    if(isset($_COOKIE['asid'])){
        $data = array(
            "token" => $_SESSION['token'],
        );
        $logout = apiCurlAction("POST","sites/".SITE_ID."/authentications/".AUTHENTICATION_ID."/logout",$data);
    }
}

function lineRichMenuChange($lineID){
    $ch = curl_init();   
    $header = array(
        'Content-Type: application/json',
        'Authorization: Bearer '.LINE_MESSAGE_TOKEN,
    );
    curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
    curl_setopt($ch, CURLOPT_URL, 'https://api.line.me/v2/bot/user/'.$lineID.'/richmenu/'.RICH_MENU_ID);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    curl_close($ch);   
    
    $json = json_decode($response);
    $userInfo= json_decode(json_encode($json), true);
    return $userInfo;
}

/**
 * V2用 API送信ロジック
 * @return Result
 */
function apiCurlAction($method, $addUrlPass, $data = null){    
    $header = array(
        "Authorization:Bearer " . API_KEY,
        "Content-Type:application/json",
        "X-Spiral-Api-Version: 1.1",
    );
    if(API_ROLE){
        $header = array_merge($header,array("X-Spiral-App-Role: ".API_ROLE));
    }

    // curl
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($curl, CURLOPT_URL, API_URL . $addUrlPass);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    if ($method == "POST") {
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
    }
    if ($method == "PATCH") {
        curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
    }
    if ($method == "DELETE") {
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
    }
    if ($method == "GET") {
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
    }
    $response = curl_exec($curl);
    if (curl_errno($curl)) echo curl_error($curl);
    curl_close($curl);
    return json_decode($response, true);
}
ポイント①
リッチメニュー切り替え
会員登録時のリッチメニュー切り替えと退会時のデフォルトリッチメニューへの変更を実装しています。
リッチメニューから認証エリア内にログインさせるつくりになっているので、実装の幅が広がっています。
完了ページでLINEのリッチメニューを切り替えるサンプルプログラム

会員用機能の実装

手順1:お問い合わせページの作成
認証エリアにお問い合わせページの作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
手順2:お問い合わせ(登録ブロック)の作成
手順1で作成したページの BODY から「+」ボタンを押下し、登録ブロックを作成します。
メールアドレス及び名前は、入力済みの状態で表示しています。
デザインに関しては、コピペCSS 「シンプルモダン」を導入しています。

HTML - body
<div class="sp-form-container"> 
  <div class="sp-form-item sp-form-html" th:inline="none"><p><span style="font-size: 18pt;">お問い合わせ</span></p></div> 
  <!--/* お名前(name) */--> 
  <sp:input-field name="f0xx"></sp:input-field> 
  <div class="sp-form-item sp-form-field"> 
    <div class="sp-form-label"> 
      <th:block th:text="${fields['f0xx'].label}">
        Label
      </th:block> 
      <span class="sp-form-required" th:if="${fields['f0xx'].required}" th:text="${fields['f0xx'].requiredIndicator}">*</span>
    </div> 
    <div class="sp-form-data">
      <input type="text" class="sp-form-control" th:name="${fields['f0xx'].name}" th:value="${#maps.containsKey(inputs, 'f0xx') ? inputs['f0xx'] : siteClient.record[xx]}">
      <span class="sp-form-noted" th:if="${fields['f0xx'].help != null}" th:text="${fields['f0xx'].help}">Help text</span>
      <span class="sp-form-error" th:if="${errors['f0xx'] != null}" th:text="${errors['f0xx'].message}">Error message</span>
    </div> 
  </div> 
  <!--/* メールアドレス(mail) */--> 
  <sp:input-field name="f0xx"></sp:input-field> 
  <div class="sp-form-item sp-form-field"> 
    <div class="sp-form-label"> 
      <th:block th:text="${fields['f0xx'].label}">
        Label
      </th:block> 
      <span class="sp-form-required" th:if="${fields['f0xx'].required}" th:text="${fields['f0xx'].requiredIndicator}">*</span>
    </div> 
    <div class="sp-form-data">      
      <input type="email" class="sp-form-control" th:name="${fields['f0xx'].name}" th:value="${#maps.containsKey(inputs, 'f0xx') ? inputs['f0xx'] : siteClient.record[xx]}"> 
      <div th:if="${fields['f0xx'].collation}">
        <span class="sp-form-email-reenter" th:text="${fields['f0xx'].reenterLabel}">Re-enter to confirm</span>
        <input type="email" class="sp-form-control" th:name="${fields['f0xx:reenter'].name}" th:value="${#maps.containsKey(inputs, 'f0xx:reenter') ? inputs['f0xx:reenter'] : siteClient.record[xx]}">
      </div> 
      <span class="sp-form-noted" th:if="${fields['f0xx'].help != null}" th:text="${fields['f0xx'].help}">Help text</span>
      <span class="sp-form-error" th:if="${errors['f0xx'] != null}" th:text="${errors['f0xx'].message}">Error message</span>
    </div> 
  </div> 
  <!--/* お問い合わせ種別(contactType) */--> 
  <sp:input-field name="f0xx"></sp:input-field> 
  <div class="sp-form-item sp-form-field"> 
    <div class="sp-form-label"> 
      <th:block th:text="${fields['f0xx'].label}">
        Label
      </th:block> 
      <span class="sp-form-required" th:if="${fields['f0xx'].required}" th:text="${fields['f0xx'].requiredIndicator}">*</span>
    </div> 
    <div class="sp-form-data"> 
      <div class="sp-form-dropdown">
        <select class="sp-form-control" th:name="${fields['f0xx'].name}">
          <option value="" th:text="${fields['f0xx'].unselectedLabel}" th:selected="${inputs['f0xx'] == null}">Select option</option>
          <option th:each="option : ${fields['f0xx'].options}" th:value="${option.id}" th:text="${option.label}" th:selected="${inputs['f0xx'] == #strings.toString(option.id)}">Item</option>
        </select>
        <span class="sp-form-dropdown-icon"></span>
      </div> 
      <span class="sp-form-noted" th:if="${fields['f0xx'].help != null}" th:text="${fields['f0xx'].help}">Help text</span>
      <span class="sp-form-error" th:if="${errors['f0xx'] != null}" th:text="${errors['f0xx'].message}">Error message</span>
    </div> 
  </div> 
  <!--/* お問い合わせ内容(contactText) */--> 
  <sp:input-field name="f0xx"></sp:input-field> 
  <div class="sp-form-item sp-form-field"> 
    <div class="sp-form-label"> 
      <th:block th:text="${fields['f0xx'].label}">
        Label
      </th:block> 
      <span class="sp-form-required" th:if="${fields['f0xx'].required}" th:text="${fields['f0xx'].requiredIndicator}">*</span>
    </div> 
    <div class="sp-form-data">
      <textarea class="sp-form-control" th:name="${fields['f0xx'].name}" th:text="${inputs['f0xx']}"></textarea>
      <span class="sp-form-noted" th:if="${fields['f0xx'].help != null}" th:text="${fields['f0xx'].help}">Help text</span>
      <span class="sp-form-error" th:if="${errors['f0xx'] != null}" th:text="${errors['f0xx'].message}">Error message</span>
    </div> 
  </div> 
  <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>
CSS に関してはコピペCSS 「シンプルモダン」の流用のため割愛いたします。
手順3:お問い合わせページの設定
手順2で作成したブロックと会員用ヘッダ・フッタをお問い合わせページに埋め込みます。
また、お問い合わせページでは完了画面で参照フィールドの更新を行うため、PHPを記載しております。
詳細については、参照フィールドをAPIで更新するサンプルプログラムを参考にしてください。

PHP
<?php
//------------------------------
// 設定値用モジュール
//------------------------------
define("API_URL", "https://api.spiral-platform.com/v1");
define("API_KEY", $SPIRAL->getEnvValue("API_KEY"));
define("API_ROLE",""); // ロールによるAPI権限が不要の場合
define("APP_ID","");
define("DB_ID","");

//------------------------------
// 設定値用モジュール
//------------------------------
$commonBase = CommonBase::getInstance();
// ステップのセット
// 登録ブロックの場合
$formComplete = $SPIRAL->getRegistrationForm(""); // 登録フォームブロックの識別名を登録
// 更新ブロックの場合
// $formComplete = $SPIRAL->getUpdateForm("ブロックの識別名"); // 更新フォームブロックの識別名を登録


// 完了画面での処理
if($formComplete->isCompletedStep()){
    $record = $SPIRAL->getRecordValue();
    $dataPATCH = array(
      "lineID" => $SPIRAL->getAuthRecordByFieldId("_id"),
    );
    $appInsert = $commonBase->apiCurlAction("PATCH","/apps/".APP_ID."/dbs/".DB_ID."/records/".$record['item']['_id'],$dataPATCH);
    if(isset($appInsert['item'])){
      $SPIRAL->setTHValue("insert", true);
    }else{
      $SPIRAL->setTHValue("insert", "error");
      $SPIRAL->setTHValue("errorText", "登録に失敗しました。");
    //   $SPIRAL->setTHValue("errorText", print_r($appInsert,true));
    }
}


//------------------------------
// 共通用モジュール
//------------------------------

class CommonBase
{
	/**
	 * シングルトンインスタンス
	 * @var UserManager
	 */
	protected static $singleton;
    

	public function __construct()
	{
		if (self::$singleton) {
			throw new Exception('must be singleton');
		}
		self::$singleton = $this;
	}

	/**
	 * シングルトンインスタンスを返す
	 * @return UserManager
	 */
	public static function getInstance()
	{
		if (!self::$singleton) {
			return new CommonBase();
		} else {
			return self::$singleton;
		}
	}

    
	/**
	 * V2用 API送信ロジック
	 * @return Result
	 */
	function apiCurlAction($method, $addUrlPass, $data = null)
	{
        
		$header = array(
			"Authorization:Bearer " . API_KEY,
			"Content-Type:application/json",
			"X-Spiral-Api-Version: 1.1",
		);
		if(API_ROLE){
			$header = array_merge($header,array("X-Spiral-App-Role: ".API_ROLE));
		}

		// curl
		$curl = curl_init();
		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_URL, API_URL . $addUrlPass);
		curl_setopt($curl, CURLOPT_HTTPHEADER, $header);

		if ($method == "POST") {
			curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
			curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
		}
		if ($method == "PATCH") {
			curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
			curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
		}
		if ($method == "DELETE") {
			curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
		}
		if ($method == "GET") {
			curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
		}
		$response = curl_exec($curl);
		if (curl_errno($curl)) echo curl_error($curl);
		curl_close($curl);

		return json_decode($response, true);
	}
}
HTML側では、ステータスが違う会員がアクセスできない制御を入れています。

HTML - head
<title th:text="${page.title}"></title>
<th:block th:if="${siteClient.record[xx]?.id} ne 2">
    <meta http-equiv="Refresh" content="0; URL=https://pb-sp2demo-application2.spiral-site.com/lineContact/reg">
</th:block>
HTML - body
<th:block th:if="${siteClient.record[xx]?.id} eq 2">
    <!-- ヘッダ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- ヘッダ -->

    <!-- コンテンツ -->
    <sp:block name="rib0xx"></sp:block>
    <!-- コンテンツ -->

    <!-- フッタ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- フッタ -->
</th:block>
手順4:自身のお問い合わせ一覧ページ作成
認証エリアにお問い合わせページの作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
手順5:自身のお問い合わせ一覧(レコードリストブロック)の作成
自身のお問い合わせを絞り込んで表示させるためには、レコード公開範囲を設定する必要があります。
エリア認証時公開のみ表示を行い、ログインフィルタにてLINE会員DBでフィルタ設定を行います。
レコード公開範囲の設定が完了しましたら、手順4で作成したページの BODY から「+」ボタンを押下し、レコードリストブロックを作成します。
レコードアイテムもしようしますので、レコードアイテム同時作成してください。
デザインに関しては、任意のものをあてていますので参考にしてください。

HTML - body
<div class="sp-record-list-container"> 
    <div class="sp-record-list-parts sp-html-parts"><p><span style="font-size: 1.5rem;">自身のお問い合わせ</span></p></div> 
    <div class="sp-record-list-parts sp-record-list-pagination"> 
      <div class="sp-record-list-pagination-left"></div> 
      <div class="sp-record-list-pagination-center"></div> 
      <div class="sp-record-list-pagination-right"> 
        <div>
          <span th:text="|${pagination.startIndex} - ${pagination.endIndex}|">Page range</span>
           / 
          <span th:text="${recordList.totalRecordCount}">Total count</span>
        </div> 
        <div class="sp-form-dropdown-inline">
          <select class="sp-form-control sp-url-switcher">
            <option>----</option>
            <option value="20" th:selected="${pagination.currentPageSize == 20}" th:attr="data-link=${pagination.getLinkWithPageSize(20)}">20件</option>
            <option value="50" th:selected="${pagination.currentPageSize == 50}" th:attr="data-link=${pagination.getLinkWithPageSize(50)}">50件</option>
          </select>
          <span class="sp-form-dropdown-icon"></span>
        </div> 
      </div> 
    </div> 
    <div class="sp-record-list-parts"> 
        <ul class="dataRecord" th:each="record, stat : ${pageRecords}">
            <sp:record-data-field name="f_id"></sp:record-data-field>
            <sp:record-data-field name="f0xx"></sp:record-data-field> 
            <li><a th:href="|${pages['p013284'].path}?${record.linkParam}|"  th:if="${record['f0xx'] != null}">
              <th:block th:each="line, stat : ${record['f0xx'].lines}"> 
                <th:block th:text="${line}"></th:block> 
                <br th:unless="${stat.last}">
              </th:block> 
            </a></li>
        </ul>
        <div class="sp-record-list-no-records" th:if="${recordList.totalRecordCount == 0}">
            表示できるデータがありません
        </div> 
    </div> 
    
    <div class="sp-record-list-parts sp-record-list-pagination"> 
      <div class="sp-record-list-pagination-left"></div> 
      <div class="sp-record-list-pagination-center"> 
        <ul class="sp-page-navs" th:with="pageNavItemSize = 5" th:if="${recordList.totalRecordCount != 0}"> 
          <li class="sp-page-nav-item" th:if="!${pagination.isFirstPage}">
            <a th:href="${pagination.firstPageLink}"><i class="fa fa-angle-double-left"></i></a>
          </li> 
          <li class="sp-page-nav-item" th:if="!${pagination.isFirstPage}">
            <a th:href="${pagination.prevPageLink}"><i class="fa fa-angle-left"></i></a>
          </li> 
          <li class="sp-page-nav-item" th:each="item : ${pagination.getPrevPageNavItems(pageNavItemSize)}">
            <a th:text="${item.pageNum}" th:href="${item.link}">Page Number</a>
          </li> 
          <li class="sp-page-nav-item sp-page-nav-current">
            <span th:text="${pagination.currentPageNum}">Page Number</span>
          </li> 
          <li class="sp-page-nav-item" th:each="item : ${pagination.getNextPageNavItems(pageNavItemSize)}">
            <a th:text="${item.pageNum}" th:href="${item.link}">Page Number</a>
          </li> 
          <li class="sp-page-nav-more" th:if="${pagination.showEllipsis}">...</li> 
          <li class="sp-page-nav-item" th:if="!${pagination.isLastPage}">
            <a th:href="${pagination.nextPageLink}"><i class="fa fa-angle-right"></i></a>
          </li> 
          <li class="sp-page-nav-item" th:if="!${pagination.isLastPage} and !${recordList.recordCountLimitExceeded}">
            <a th:href="${pagination.lastPageLink}"><i class="fa fa-angle-double-right"></i></a>
          </li> 
        </ul> 
      </div> 
      <div class="sp-record-list-pagination-right"></div> 
    </div> 
  </div>
HTML - css
.sp-record-list-container .sp-record-list-parts{
    color: #333333;
    background-color: #ffffff;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    margin: 90px auto 25px;
    padding: 0 25px;
}

ul.dataRecord {
  padding: 0 25px;
}

ul.dataRecord li {
  display: block;
}
ul.dataRecord li a {
  position: relative;
  display: block;
  padding: 10px 25px 10px 10px;
  border-bottom: 1px solid #ccc;
}
ul.dataRecord li a::after {
  position: absolute;
  top: 50%;
  right: 10px;
  display: block;
  content: '';
  width: 8px;
  height: 8px;
  margin-top: -4px;
  border-top: 1px solid #888;
  border-right: 1px solid #888;
  -webkit-transform: rotate(45deg);
  transform: rotate(45deg);
}
/*  */




.sp-record-list-container {
  color:#333333;
  background-color:#ffffff;
  width:100%;
  margin:90px auto 25px;
}

.sp-record-list-container .sp-record-list-no-records {
  margin:6.25rem auto;
  text-align:center;
  color:#808080;
}

.sp-record-list-pagination {
  display:-webkit-box;
  display:-ms-flexbox;
  display:flex;
  -webkit-box-pack:justify;
  -ms-flex-pack:justify;
  justify-content:space-between;
  -webkit-box-align:center;
  -ms-flex-align:center;
  align-items:center;
}

.sp-record-list-pagination .sp-record-list-pagination-left,
.sp-record-list-pagination .sp-record-list-pagination-center,
.sp-record-list-pagination .sp-record-list-pagination-right {
  display:-webkit-box;
  display:-ms-flexbox;
  display:flex;
  -webkit-box-align:center;
  -ms-flex-align:center;
  align-items:center;
}

.sp-record-list-pagination .sp-record-list-pagination-left>*,
.sp-record-list-pagination .sp-record-list-pagination-center>*,
.sp-record-list-pagination .sp-record-list-pagination-right>* { margin:0 .5rem; }

.sp-record-list-pagination .sp-record-list-pagination-left {
  -webkit-box-pack:start;
  -ms-flex-pack:start;
  justify-content:flex-start;
}

.sp-record-list-pagination .sp-record-list-pagination-center {
  -webkit-box-pack:center;
  -ms-flex-pack:center;
  justify-content:center;
}

.sp-record-list-pagination .sp-record-list-pagination-right {
  -webkit-box-pack:end;
  -ms-flex-pack:end;
  justify-content:flex-end;
}

.sp-record-list-pagination .sp-form-control {
  -webkit-appearance:none;
  -moz-appearance:none;
  appearance:none;
  font-family:inherit;
  font-size:1rem;
  line-height:1.5;
  padding:.3rem .7rem;
  color:#333333;
  -webkit-box-sizing:border-box;
  box-sizing:border-box;
  width:100%;
  margin-bottom:0.2rem;
  border:1px solid #dddddd;
  border-radius:.25rem;
}

.sp-record-list-pagination .sp-form-inline .sp-form-control {
  display:inline-block;
  width:auto;
  vertical-align:middle;
}

.sp-record-list-pagination select.sp-form-control,
.sp-record-list-pagination option.sp-form-control {
  -webkit-appearance:none;
  -moz-appearance:none;
  appearance:none;
  padding-right:1.5rem;
}

.sp-record-list-pagination select.sp-form-control::-ms-expand { display:none; }

.sp-record-list-pagination .sp-form-dropdown { position:relative; }

.sp-record-list-pagination .sp-form-dropdown-inline {
  position:relative;
  display:inline-block;
}

.sp-record-list-pagination .sp-form-dropdown-icon {
  display:block;
  position:absolute;
  top:1rem;
  right:.5rem;
  line-height:0;
  pointer-events:none;
}

.sp-record-list-pagination .sp-form-dropdown-icon:after {
  content:"";
  display:block;
  border-top:.333rem solid #888888;
  border-right:.333rem solid transparent;
  border-left:.333rem solid transparent;
}

.sp-record-list-pagination ul.sp-page-navs {
  display:-webkit-box;
  display:-ms-flexbox;
  display:flex;
  padding-left:0;
  list-style:none;
}

.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item {
  line-height:1.25rem;
  text-align:center;
  -webkit-user-select:none;
  -moz-user-select:none;
  -ms-user-select:none;
  user-select:none;
  white-space:nowrap;
}

.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item a,
.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item span {
  display:inline-block;
  margin:0 0.2rem;
  padding:.375rem .75rem;
  border-radius:.25rem;
  border:1px solid #dddddd;
  color:#333333;
}

.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item a .fa,
.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item span .fa {
  font-size:1.125rem;
  width:1rem;
}

.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item a { text-decoration:none; }

.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item a:hover { background-color:rgba(0,0,0,0.075); }

.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item span {
  background-color:rgba(0,0,0,0.075);
  color:#808080;
}

.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-item.sp-page-nav-current span {
  background-color:#555555;
  border-color:#555555;
  color:#ffffff;
}

.sp-record-list-pagination ul.sp-page-navs li.sp-page-nav-more {
  line-height:1rem;
  text-align:center;
  margin:0 .2rem;
  padding:.375rem .3rem;
}
手順5で作成したブロックと会員用ヘッダ・フッタをお問い合わせページに埋め込み完成です。
こちらでもステータスが違う会員がアクセスできない制御を入れています。

HTML - head
<title th:text="${page.title}"></title>
<th:block th:if="${siteClient.record[xx]?.id} ne 2">
    <meta http-equiv="Refresh" content="0; URL=https://pb-sp2demo-application2.spiral-site.com/lineContact/reg">
</th:block>
HTML - body
<th:block th:if="${siteClient.record[xx]?.id} eq 2">
    <!-- ヘッダ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- ヘッダ -->

    <!-- コンテンツ -->
    <sp:block name="rib0xx"></sp:block>
    <!-- コンテンツ -->

    <!-- フッタ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- フッタ -->
</th:block>
手順4:自身のお問い合わせ一覧ページ作成
認証エリアにお問い合わせページの作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
手順6:自身のお問い合わせ詳細(レコードアイテムブロック)の設定
自身のお問い合わせ一覧を作成した際に、自身のお問い合わせ詳細のぺージ及びブロックが作成されています。
ブロックのデザイン反映と ぺージ側でレコードアイテムブロックと会員用ヘッダ・フッタを埋め込むだけで、詳細ページが完成となります。

HTML - body
<div class="sp-record-list-container"> 
      <a th:each="record, stat : ${pageRecords}" th:href="|${pages['p0xx']?.path ?: '/404'}?${record.linkParam}|" >
            <article>
                  <sp:record-data-field name="f0xx"></sp:record-data-field>
                  <p class="text_date" th:text="${record['f0xx'] != null} ? ${record['f0xx'].format('yyyy/MM/dd')} : ''"><time  th:datetime="${record['f0xx'] != null} ? ${record['f0xx'].format('yyyy/MM/dd')} : ''">2022.01.01</time></p>
                  <sp:record-data-field name="f0xx"></sp:record-data-field>
                  <h2 th:text="${record['f0xx']}">タイトル</h2>
            </article>
        </a>
  <div class="sp-record-list-no-records" th:if="${recordList.totalRecordCount == 0}">
        表示できるデータがありません
    </div> 
</div>

HTML - css
.sp-record-list-container {
  color:#333333;
  background-color:#ffffff;
}

.sp-record-list-container a{
  text-decoration: none;
}

h1 {
  display: inline-block;
  margin: 0 auto 40px;
  padding-bottom: 10px;
  font-size: 1.25rem;
  font-weight: 700;
  border-bottom: 4px solid #222;
}

article {
  margin: 0 25px 40px;
  padding: 40px 40px 35px;
  text-align: left;
  border-radius: 20px;
  background-color: #e1e1e1;
}

article .text_date {
  font-size: 0.85rem;
  color:#333;
}

article h2 {
  margin-bottom: 30px;
  padding-bottom: 12px;
  font-size: 1.15rem;
  line-height: 1.8em;
  /* border-bottom: 2px solid #f04244; */
  color:#333;
}
手順7:お知らせ一覧ページ作成
認証エリアにお問い合わせページの作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
お知らせページは会員用と非会員用の2ページ作成を行います。
ページを作成する際に非会員用は、全体公開となるので、認証なしを選択してください。
手順8:お知らせ一覧(レコードリストブロック)の作成
お知らせ一覧を表示する際にも、レコード公開範囲を設定する必要があります。
一般公開時には、表示ステータスが「全体に表示」と等しいものをフィルタし、
エリア認証時公開範囲 には、表示ステータスが「値があり」のものでフィルタします。
レコード公開範囲の設定が完了しましたら、
手順4で作成したページの BODY から「+」ボタンを押下し、レコードリストブロックを作成します。
デザインに関しては、任意のものをあてていますので参考にしてください。

HTML - body
<div class="sp-record-item-container" th:if="${record != null}"> 
  <div class="sp-record-item-parts sp-html-parts"><p><span style="font-size: 1.5rem;">自身のお問い合わせ</span></p></div> 
  <!--/* (ID) (_id) */--> 
  <sp:record-item-field name="f_id"></sp:record-item-field> 
  <div class="sp-record-item-parts sp-record-item-field"> 
    <div class="sp-record-item-label" th:text="${fields['f_id'].label}">Label</div> 
    <div class="sp-record-item-data">
      <span class="sp-record-item-embedded" th:text="${record['f_id']}">12345</span>
    </div> 
  </div> 
  <!--/* お問い合わせステータス (status) */--> 
  <sp:record-item-field name="f0xx"></sp:record-item-field> 
  <div class="sp-record-item-parts sp-record-item-field"> 
    <div class="sp-record-item-label" th:text="${fields['f0xx'].label}">Label</div> 
    <div class="sp-record-item-data">
      <span class="sp-record-item-embedded" th:text="${record['f0xx'] != null} ? ${fields['f0xx'].optionMap[record['f0xx'].id]} : ''">Item</span>
    </div> 
  </div> 
  <!--/* お名前 (name) */--> 
  <sp:record-item-field name="f0xx"></sp:record-item-field> 
  <div class="sp-record-item-parts sp-record-item-field"> 
    <div class="sp-record-item-label" th:text="${fields['f0xx'].label}">Label</div> 
    <div class="sp-record-item-data">
      <span class="sp-record-item-embedded" th:text="${record['f0xx']}">Example</span>
    </div> 
  </div> 
  <!--/* メールアドレス (mail) */--> 
  <sp:record-item-field name="f0xx"></sp:record-item-field> 
  <div class="sp-record-item-parts sp-record-item-field"> 
    <div class="sp-record-item-label" th:text="${fields['f0xx'].label}">Label</div> 
    <div class="sp-record-item-data">
      <span class="sp-record-item-embedded" th:text="${record['f0xx']}">spiral@example.com</span>
    </div> 
  </div> 
  <!--/* お問い合わせ内容 (contactText) */--> 
  <sp:record-item-field name="f0xx"></sp:record-item-field> 
  <div class="sp-record-item-parts sp-record-item-field"> 
    <div class="sp-record-item-label" th:text="${fields['f0xx'].label}">Label</div> 
    <div class="sp-record-item-data">
      <span class="sp-record-item-embedded" th:if="${record['f0xx'] != null}"> 
        <th:block th:each="line, stat : ${record['f0xx'].lines}"> 
          <th:block th:text="${line}"></th:block> 
          <br th:unless="${stat.last}">
        </th:block> 
      </span>
    </div> 
  </div> 
  <!--/* お問い合わせ回答 (contactAnswer) */--> 
  <sp:record-item-field name="f0xx"></sp:record-item-field> 
  <div class="sp-record-item-parts sp-record-item-field"> 
    <div class="sp-record-item-label" th:text="${fields['f0xx'].label}">Label</div> 
    <div class="sp-record-item-data">
      <span class="sp-record-item-embedded" th:if="${record['f0xx'] != null}"> 
        <th:block th:each="line, stat : ${record['f0xx'].lines}"> 
          <th:block th:text="${line}"></th:block> 
          <br th:unless="${stat.last}">
        </th:block> 
      </span>
    </div> 
  </div> 
</div> 
<div class="sp-record-item-container" th:if="${record == null}"> 
  <div class="sp-record-item-no-item">
    データが見つかりません
  </div> 
</div>
HTML - css
.sp-record-item-container {
  padding:0 25px;
}

.sp-record-item-container .sp-record-item-parts {
  margin-bottom:0.5rem;
  padding-bottom:0.5rem;
}

.sp-record-item-container .sp-record-item-parts:last-child.sp-record-item-field { border-bottom:0; }

.sp-record-item-container .sp-record-item-field {
  display:-webkit-box;
  display:-ms-flexbox;
  display:flex;
  -webkit-box-orient:vertical;
  -webkit-box-direction:normal;
  -ms-flex-direction:column;
  flex-direction:column;
  border-bottom:1px solid #dddddd;
}

.sp-record-item-container .sp-record-item-field>.sp-record-item-label {
  font-size:1.15rem;
  font-weight:bold;
  padding:.25rem .5rem;
  word-break:break-all;
  -webkit-box-sizing:border-box;
  box-sizing:border-box;
  color:#555555;
}

.sp-record-item-container .sp-record-item-field>.sp-record-item-data {
  display:-webkit-box;
  display:-ms-flexbox;
  display:flex;
  -webkit-box-orient:vertical;
  -webkit-box-direction:normal;
  -ms-flex-direction:column;
  flex-direction:column;
  padding:1rem;
  -webkit-box-sizing:border-box;
  box-sizing:border-box;
}

.sp-record-item-container .sp-record-item-field .sp-record-item-embedded { padding-left:.25rem; }

.sp-record-item-container .sp-record-item-data-selection {
  display:block;
  margin:.25rem 0;
  padding:.5rem .8rem .5rem 0;
  border-radius:0.25rem;
}

.sp-record-item-container .sp-html-parts p { margin:0; }

.sp-record-item-container .sp-record-item-no-item {
  margin:6.25rem auto;
  text-align:center;
  color:#808080;
}

.sp-record-item-field:first-child,
.sp-record-item-field-ie:first-child { padding-top:0 !important; }

.sp-record-item-field:last-child,
.sp-record-item-field-ie:last-child { border-bottom:none; }

.sp-file-info {
  padding-top:1em;
  padding-bottom:1em;
  line-height:1.5;
}

.sp-file-info:last-child { padding-bottom:0; }

.sp-form-file-bold-link {
  text-decoration:none !important;
  color:#3b7e9b;
  cursor:pointer;
  font-weight:bold;
}

.sp-form-file-size-label { color:#6c757d; }

.sp-text-truncate {
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

@media (min-width:768px) {
  .sp-record-item-container {
    font-size:initial;
    padding:3rem;
  }

  .sp-record-item-container .sp-record-item-field {
    -webkit-box-orient:horizontal;
    -webkit-box-direction:normal;
    -ms-flex-flow:row nowrap;
    flex-flow:row nowrap;
  }

  .sp-record-item-container .sp-record-item-field>.sp-record-item-label {
    font-size:1rem;
    width:30%;
    padding:1rem;
  }

  .sp-record-item-container .sp-record-item-field>.sp-record-item-data {
    font-size:1rem;
    width:70%;
  }

  .sp-record-item-container .sp-record-item-field .sp-record-item-embedded { font-size:.9rem; }

  .sp-record-item-container .sp-record-item-data-selection {
    font-size:.9rem;
    margin:0 1rem 0 0;
    padding:0;
  }

  .sp-record-item-container .sp-record-item-data-selection-vertical {
    display:-webkit-box;
    display:-ms-flexbox;
    display:flex;
    -webkit-box-orient:vertical;
    -webkit-box-direction:normal;
    -ms-flex-direction:column;
    flex-direction:column;
  }

  .sp-record-item-container .sp-record-item-data-selection-horizontal {
    display:-webkit-box;
    display:-ms-flexbox;
    display:flex;
    -webkit-box-orient:horizontal;
    -webkit-box-direction:normal;
    -ms-flex-flow:row wrap;
    flex-flow:row wrap;
  }
}
手順9:お知らせ一覧ページ作成
手順7で作成したブロックをページに埋め込みます。
非会員用のページでは、非会員用ヘッダ・フッタを埋め込み、会員用のページでは、会員用ヘッダ・フッタを埋め込みます。
同じブロックを使用しますが、レコード公開範囲で表示レコードが出し分けれるつくりになります。
手順10:ユーザ通知ページ作成
認証エリアにお問い合わせページの作成を行います。
識別名がURLのディレクトリ名になるので、使用したいURLの階層に合わせて設定してください。
手順11:ユーザ通知一覧(レコードリストブロック)の作成
ログインユーザの通知のみ表示させるためには、レコード公開範囲を設定する必要があります。
エリア認証時公開のみ表示を行い、ログインフィルタにてLINE会員DBでフィルタ設定を行います。
レコード公開範囲の設定が完了しましたら、
手順4で作成したページの BODY から「+」ボタンを押下し、レコードリストブロックを作成します。
デザインに関しては、任意のものをあてていますので参考にしてください。

HTML - body
<div class="sp-record-list-container"> 
  <article th:each="record, stat : ${pageRecords}">
        <sp:record-data-field name="f0xx"></sp:record-data-field>
        <p class="text_date" th:text="${record['f0xx'] != null} ? ${record['f0xx'].format('yyyy/MM/dd')} : ''"><time  th:datetime="${record['f0xx'] != null} ? ${record['f0xx'].format('yyyy/MM/dd')} : ''">2022.01.01</time></p>
        <sp:record-data-field name="f0xx"></sp:record-data-field>
        <h2 th:text="${record['f0xx']}">タイトル</h2>
        <sp:record-data-field name="f0xx"></sp:record-data-field>
        <p class="sp-record-item-embedded" th:if="${record['f0xx'] != null}"> 
        <th:block th:each="line, stat : ${record['f0xx'].lines}"> 
          <th:block th:text="${line}"></th:block> 
          <br th:unless="${stat.last}">
        </th:block> 
       </p>
  </article>
  <div class="sp-record-list-no-records" th:if="${recordList.totalRecordCount == 0}">
        表示できるデータがありません
    </div> 
</div>
HTML - css
.sp-record-list-container {
  color:#333333;
  background-color:#ffffff;}
h1 {
  display: inline-block;
  margin: 0 auto 40px;
  padding-bottom: 10px;
  font-size: 1.25rem;
  font-weight: 700;
  border-bottom: 4px solid #222;
}

article {
  margin: 0 25px 40px;
  padding: 40px 40px 35px;
  text-align: left;
  border-radius: 20px;
  background-color: #e1e1e1;
}

article .text_date {
  font-size: 0.85rem;
}

article h2 {
  margin-bottom: 30px;
  padding-bottom: 12px;
  font-size: 1.15rem;
  line-height: 1.8em;
  border-bottom: 2px solid #f04244;
}

article .text_content {
  line-height: 2.0em;
  font-size: 0.85rem;
}
作成したブロックを手順9で作成したページに埋め込み完了となります。
また、お知らせでもユーザ通知とタブの形での出し分けが行われるため、タブのデザインの反映とステータスが違う会員がアクセスできない制御を入れています。

HTML - head
<th:block th:if="${siteClient.record[xx]?.id} eq 2">
    <!-- ヘッダ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- ヘッダ -->

    <!-- タブメニュー -->
    <div class="menuTab">
      <div class="tabItem active">通知</div>
      <a href="/lineContact/userNews"><div class="tabItem">お知らせ</div></a>
    </div>

    <!-- コンテンツ -->
    <sp:block name="rlb0xx"></sp:block>
    <!-- コンテンツ -->

    <!-- フッタ -->
    <sp:block name="fcb0xx"></sp:block>
    <!-- フッタ -->
</th:block>
HTML - body
.menuTab{
  margin: 0 40px;
  height: auto;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: flex-end;
}
.menuTab a{
  text-decoration: none;
}

.menuTab .tabItem{
    padding: 10px 10px;
    text-align: center;
    background: #505050;
    font-size: 18px;
    color: #fff;
    border-top: none;
    border-left: none;
    border-right: none;
    border-bottom: solid 3px #505050;
    border-top-left-radius: 8px;
    border-top-right-radius: 8px;
    width: 150px;
}

@media (max-width: 768px){
    .menuTab .tabItem{
        padding: 10px 20px;
        width: auto;
    }
}


.menuTab .tabItem.active{
  background: #fff;
  color: #505050;
  border-top: solid 3px #505050;
  border-left: solid 3px #505050;
  border-right: solid 3px #505050;
  border-bottom: none;
  font-weight: bold;
  font-size: 20px;
}
手順12:ユーザ通知詳細(レコードアイテムブロック)の設定
ユーザ通知一覧を作成した際に、ユーザ通知詳細のぺージ及びブロックが作成されています。
最後にブロックのデザイン反映と ぺージ側でレコードアイテムブロックと会員用ヘッダ・フッタを埋め込むだけで、詳細ページが完成となります。
レコードアイテムブロックのデザインはお問い合わせ詳細と同じデザインになります。

HTML - body
<div class="sp-record-list-container"> 
      <a th:each="record, stat : ${pageRecords}" th:href="|${pages['p0xx']?.path ?: '/404'}?${record.linkParam}|" >
            <article>
                  <sp:record-data-field name="f0xx"></sp:record-data-field>
                  <p class="text_date" th:text="${record['f0xx'] != null} ? ${record['f0xx'].format('yyyy/MM/dd')} : ''"><time  th:datetime="${record['f0xx'] != null} ? ${record['f0xx'].format('yyyy/MM/dd')} : ''">2022.01.01</time></p>
                  <sp:record-data-field name="f0xx"></sp:record-data-field>
                  <h2 th:text="${record['f0xx']}">タイトル</h2>
            </article>
        </a>
  <div class="sp-record-list-no-records" th:if="${recordList.totalRecordCount == 0}">
        表示できるデータがありません
    </div> 
</div>

HTML - css
.sp-record-list-container {
  color:#333333;
  background-color:#ffffff;
}

.sp-record-list-container a{
  text-decoration: none;
}

h1 {
  display: inline-block;
  margin: 0 auto 40px;
  padding-bottom: 10px;
  font-size: 1.25rem;
  font-weight: 700;
  border-bottom: 4px solid #222;
}

article {
  margin: 0 25px 40px;
  padding: 40px 40px 35px;
  text-align: left;
  border-radius: 20px;
  background-color: #e1e1e1;
}

article .text_date {
  font-size: 0.85rem;
  color:#333;
}

article h2 {
  margin-bottom: 30px;
  padding-bottom: 12px;
  font-size: 1.15rem;
  line-height: 1.8em;
  /* border-bottom: 2px solid #f04244; */
  color:#333;
}
ポイント①
特定のユーザに通知する一覧
ユーザ通知一覧では、管理画面で選択したユーザのみ表示される一覧を生成しています。
管理者側からユーザに向けてメッセージを発信したい場合などに使用できます。

最後に

設定後は動作確認を必ず行い、動作に問題がないか確認をしてください。
また、不具合やほかのやり方が知りたい等あれば、下記の「コンテンツに関しての要望はこちら」からご連絡ください。
サイト設計・構築が完了したので次はユーザ・アプリロール・グループ設定に進みます。

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