「LWRサイト × Agentforce」 不動産ポータル開発連載、第3回です。 前回(Vol.2)は、自作スライダーによるリッチな募集住戸詳細画面を作りました。今回はその詳細画面に埋め込む、「リアルタイム内見予約カレンダー」 の実装です。
従来の不動産サイトによくある「この物件について問い合わせる」というフォームは、B2B(仲介業者間)の取引においてはスピード感が足りません。 電話確認なしで、画面上で空き枠(◎)をクリックすればその場で予約が確定する。そんなシステムを構築しました。
今回の記事で作成したコンポーネントの全ソースコードをGitHubで公開しています。 ぜひ Clone して、あなたの組織で動かしてみてください。
完成イメージとアーキテクチャ
実装したカレンダーがこちらです(画面内右下)。
縦軸に「時間」、横軸に「日付(1週間)」をとったマトリクス表示です。

予約できることを意味する [◎] ボタンを押すと予約フォームが開きます。

このUIを実現するために、以下のアーキテクチャを採用しました。
- Apex: 予約データ(
ViewingRequest__c)を検索し、週間マトリクスデータ(JSON)を作成して返す。 - LWC (Calendar): マトリクスを描画し、週の切り替え(前へ/次へ)を制御する。
- LWC (Modal): ボタンをクリックした際に、入力フォームをポップアップさせる。
カレンダーの「日付計算」と「モバイル対応」
カレンダー実装で一番面倒なのが「日付計算」です。 今回は connectedCallback で、「今日が何曜日であっても、強制的にその週の月曜日を開始日にする」 ロジックを組んでいます。
listingAvailabilityCalendar.js
connectedCallback() {
// 今日の日付をベースに「今週の月曜日」を計算
const today = new Date();
const day = today.getDay(); // 0(Sun) - 6(Sat)
// 月曜始まりにするための調整値
const diff = day === 0 ? -6 : 1 - day;
today.setDate(today.getDate() + diff);
today.setHours(0, 0, 0, 0);
this.currentStartDate = today;
this.loadCalendar();
}
listingAvailabilityCalendar.css
また、このカレンダーはスマホでも閲覧されます。7日分の列をスマホの狭い画面に収めるのは不可能です。 そこで、「横スクロール」 を許容しつつ、「時間の列(左端)だけは固定する」 というUXを採用しました。これを実現するのが CSSの position: sticky です。
/* 左端(時間)列を固定する */
.sticky-column {
position: sticky;
left: 0;
z-index: 10; /* 他のセルより前面に配置 */
background-color: var(--dxp-g-root, #fff); /* 透過防止 */
border-right: 1px solid #dddbda; /* 境界線で見やすくする */
}

最新の Lightning Modal 実装
予約フォームとなるモーダル画面 tourReservationModal は、標準の LightningModal を継承して作成します。 Aura時代のように「HTMLの中にモーダルを書いて if:true で出し分け」たりする必要はありません。
tourReservationModal.js
import LightningModal from 'lightning/modal';
export default class TourReservationModal extends LightningModal {
@api listingId;
@api initialTime; // 親から渡されるクリックした日時
// ...
}
実践テクニック:reduceを使った一括バリデーション
フォーム送信時の入力チェック(必須項目など)には、配列メソッドの reduce を使うと非常にスマートに書けます。
async handleSubmit() {
// 全てのinputコンポーネントを取得してチェック
const allValid = [...this.template.querySelectorAll('lightning-input, lightning-textarea')]
.reduce((validSoFar, inputCmp) => {
inputCmp.reportValidity();
return validSoFar && inputCmp.checkValidity();
}, true);
if (!allValid) {
return;
}
// ... 送信処理へ
}
親コンポーネントからの呼び出しフロー
カレンダー側(親)では、このモーダルを open() メソッドで呼び出し、結果を await で待ち受けます。 非常に読みやすい、モダンな非同期フローになっています。
listingAvailabilityCalendar.js
async handleSlotClick(event) {
const selectedTime = event.target.dataset.time;
// モーダルを開き、ユーザーの操作完了を待つ (Promise)
const result = await TourReservationModal.open({
size: 'medium',
description: '内見予約フォーム',
listingId: this.recordId,
initialTime: selectedTime
});
// モーダルから { status: 'success' } が返ってきたら完了アラートを出す
if (result && result.status === 'success') {
await LightningAlert.open({
message: '内見のお申し込みを受け付けました。',
theme: 'success',
label: '予約完了',
});
// カレンダーを再読み込みして「×」に更新する
this.loadCalendar();
}
}
まとめ
Vol.3では、LWRサイトにおけるリアルタイム予約機能を実装しました。
- JSでの日付操作: 週初めの計算ロジック。
- CSS Sticky: モバイルでも使いやすいテーブルUI。
- Lightning Modal:
open/closeを使ったきれいなデータ受け渡しとバリデーション。
これで、ユーザーは物件を探し、詳細を見て、その場で予約できるようになりました。 次回 Vol.4 は、いよいよ最新技術 Agentforce の登場です。 ここまでは「ユーザーが自分で探す」機能でしたが、次は「AIエージェントに探させる」機能を実装します。しかしそこには API制限の壁 が…。その突破方法を解説します。





読者の声