ホームページ  >  記事  >  ウェブフロントエンド  >  ゼロからヒーローへ: 不動産賃貸ウェブサイトとモバイル アプリを構築する私の旅

ゼロからヒーローへ: 不動産賃貸ウェブサイトとモバイル アプリを構築する私の旅

Susan Sarandon
Susan Sarandonオリジナル
2024-11-11 16:09:031020ブラウズ

コンテンツ

  1. はじめに
  2. 技術スタック
  3. 概要
  4. ライブデモ
  5. API
  6. フロントエンド
  7. モバイルアプリ
  8. 管理者ダッシュボード
  9. 名所
  10. リソース

ソースコード: https://github.com/aelassas/movinin

デモ: https://movinin.dynv6.net:3004

導入

このアイデアは、あらゆる側面をコントロールできる、完全にカスタマイズ可能で運用可能な不動産賃貸 Web サイトとモバイル アプリを境界線なく構築したいという願望から生まれました。

  • UI/UX を所有する: テンプレートの制限と戦うことなく、独自の顧客エクスペリエンスをデザインします
  • バックエンドの制御: 要件に完全に一致するカスタム ビジネス ロジックとデータ構造を実装します
  • マスター DevOps: 優先ツールとワークフローを使用してアプリケーションをデプロイ、拡張、監視します
  • 自由に拡張: プラットフォームの制約や追加料金なしで、新しい機能や統合を追加します

技術要件:

  • 支払いゲートウェイ:
    • 国際的にサポートされる安全な支払いゲートウェイを実装します
    • 複数の国と通貨間での互換性を確保
  • DevOps:
    • 一貫性とスケーラビリティのために Docker コンテナを使用してデプロイします
    • 最小限のインフラストラクチャ (1GB RAM サーバー) でホスト
    • Hetzner や DigitalOcean などのプロバイダーを使用して、月額ホスティング費用を 5 ドル未満に維持します
    • 効率的な運用のためにリソースの使用を最適化します

技術スタック

これを可能にした技術スタックは次のとおりです:

  • TypeScript
  • Node.js
  • MongoDB
  • 反応
  • ムイ
  • ネイティブに反応
  • エキスポ
  • ストライプ
  • ドッカー

TypeScript には多くの利点があるため、設計上の重要な決定事項として TypeScript を使用することが決定されました。 TypeScript は強力な型指定、ツール、統合を提供し、その結果、デバッグとテストが容易な、高品質でスケーラブルで読みやすく保守しやすいコードが得られます。

私は、強力なレンダリング機能のために React を、柔軟なデータ モデリングのために MongoDB を、そして安全な支払い処理のために Stripe を選びました。

このスタックを選択すると、Web サイトやモバイル アプリを構築するだけでなく、堅牢なオープンソース テクノロジーと成長する開発者コミュニティに支えられ、ニーズに合わせて進化できる基盤に投資することになります。

React は、次の理由から優れた選択肢として際立っています。

  1. コンポーネントベースのアーキテクチャ
    • 複雑な UI をより小さく再利用可能な部分に分割できます
    • コードの保守性が向上し、テストが容易になります
    • コードの編成と再利用性を向上させます
  2. 仮想 DOM パフォーマンス
    • React の仮想 DOM は必要なものだけを効率的に更新します
    • ページの読み込みが速くなり、ユーザー エクスペリエンスが向上します
    • 不必要な再レンダリングを削減します
  3. 豊かなエコシステム
    • 事前に構築されたコンポーネントの膨大なライブラリ
    • 豊富なツール
    • サポートとリソースのための大規模なコミュニティ
  4. 強力な開発者経験
    • 即時フィードバックのためのホットリロード
    • 優れたデバッグ ツール
    • JSX により、UI コードの記述がより直感的になります
  5. 業界サポート
    • Meta (旧 Facebook) によって支援されています
    • 多くの大手企業が使用しています
    • 継続的な開発と改善
  6. 柔軟性
    • 小規模なアプリケーションと大規模なアプリケーションの両方に適しています
    • 既存のプロジェクトに段階的に統合可能
    • 複数のレンダリング戦略 (クライアント側、サーバー側、静的) をサポート

概要

このセクションでは、フロントエンド、管理ダッシュボード、モバイル アプリのメイン ページが表示されます。

フロントエンド

顧客はフロントエンドから利用可能な物件を検索し、物件を選択してチェックアウトできます。

以下はフロントエンドのメイン ページで、顧客はここで場所と時間を指定し、利用可能なプロパティを検索できます。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、お客様が賃貸物件を選択できるメインページの検索結果です。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

お客様が物件の詳細を閲覧できるページは以下の通りです:

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は物件の画像です:

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、顧客がレンタル オプションを設定してチェックアウトできるチェックアウト ページです。顧客が登録されていない場合は、チェックアウトと登録を同時に行うことができます。まだ登録していない場合は、パスワードを設定するための確認およびアクティベーション電子メールが送信されます。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下はサインインページです。運用環境では、認証 Cookie は httpOnly、署名付き、安全かつ厳密な SameSite です。これらのオプションは、XSS、CSRF、および MITM 攻撃を防止します。認証 Cookie は、カスタム ミドルウェアを通じて XST 攻撃からも保護されます。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下はサインアップページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、顧客が予約を確認および管理できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、お客様が予約の詳細を確認できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、顧客が通知を確認できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、お客様が設定を管理できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、顧客がパスワードを変更できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

それだけです。それがフロントエンドのメインページです。

管理者ダッシュボード

3 つのタイプのユーザー:

  • 管理者: 管理者ダッシュボードへの完全なアクセス権があります。彼らは何でもできます。
  • 代理店: 管理ダッシュボードへのアクセスは制限されています。彼らは自分の施設、予約、顧客のみを管理できます。
  • 顧客: フロントエンドとモバイル アプリのみにアクセスできます。管理者ダッシュボードにはアクセスできません。

このプラットフォームは複数の代理店と連携できるように設計されています。各代理店は、管理ダッシュボードからその施設、顧客、予約を管理できます。このプラットフォームは、1 つの代理店のみと連携することもできます。

管理者はバックエンドから、代理店、施設、場所、顧客、予約を作成および管理できます。

新しい代理店が作成されると、施設、顧客、予約を管理できるように、管理ダッシュボードにアクセスするためのアカウントを作成するよう求めるメールが届きます。

以下は管理者ダッシュボードのサインイン ページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、管理者や代理店が予約を確認および管理できるダッシュボード ページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

予約のステータスが変更された場合、関連する顧客に通知とメールが届きます。

以下はプロパティが表示され、管理できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、管理者や代理店が画像や物件情報を提供して新しい物件を作成できるページです。無料でキャンセルする場合は、0 に設定します。それ以外の場合は、オプションの価格を設定するか、オプションを含めたくない場合は空のままにしてください。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、管理者と代理店がプロパティを編集できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、管理者が顧客を管理できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、代理店が管理ダッシュボードから予約を作成したい場合に予約を作成するページです。それ以外の場合、フロントエンドまたはモバイル アプリからチェックアウト プロセスが完了すると、予約は自動的に作成されます。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は予約を編集するページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は代理店を管理するページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は新しい代理店を作成するページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は代理店を編集するページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は代理店の物件を表示するページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は顧客の予約を確認するページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

以下は、管理者と代理店が設定を管理できるページです。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

他にもページがありますが、これらが管理者ダッシュボードのメインページです。

それだけです。これが管理者ダッシュボードのメイン ページです。

ライブデモ

フロントエンド

  • URL: https://movinin.dynv6.net:3004/
  • ログイン: jdoe@movinin.io
  • パスワード: M00vinin

管理者ダッシュボード

  • URL: https://movinin.dynv6.net:3003/
  • ログイン: admin@movinin.io
  • パスワード: M00vinin

モバイルアプリ

Android アプリは任意の Android デバイスにインストールできます。

このコードをデバイスでスキャンします

カメラ アプリを開き、このコードをポイントします。次に、表示される通知をタップします。

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

Androidにモバイルアプリをインストールする方法

  • Android 8.0 (API レベル 26) 以降を実行しているデバイスでは、[不明なアプリのインストール] システム設定画面に移動して、特定の場所 (つまり、アプリのダウンロード元の Web ブラウザー) からのアプリのインストールを有効にする必要があります。 .

  • Android 7.1.1 (API レベル 25) 以下を実行しているデバイスでは、[設定] > [提供元不明のシステム] 設定を有効にする必要があります。デバイスのセキュリティ。

別の方法

APK を直接ダウンロードして Android デバイスにインストールすることで、Android アプリをインストールすることもできます。

  • APK をダウンロード
  • ログイン: jdoe@movinin.io
  • パスワード: M00vinin

API

From Zero to Hero: My Journey Building a Property Rental Website and Mobile App

API は、管理ダッシュボード、フロントエンド、モバイル アプリに必要なすべての機能を公開します。 API は MVC 設計パターンに従っています。認証にはJWTが使用されます。プロパティ、予約、顧客の管理に関連する機能など、認証が必要な機能と、認証されていないユーザーが場所や利用可能なプロパティを取得するなど、認証を必要としない機能があります。

  • ./api/src/models/ フォルダーには MongoDB モデルが含まれています。
  • ./api/src/routes/ フォルダーには Express ルートが含まれています。
  • ./api/src/controllers/ フォルダーにはコントローラーが含まれています。
  • ./api/src/middlewares/ フォルダーにはミドルウェアが含まれています。
  • ./api/src/config/env.config.ts には、構成と TypeScript タイプの定義が含まれています。
  • ./api/src/lang/ フォルダーにはローカリゼーションが含まれています。
  • ./api/src/app.ts はルートがロードされるメインサーバーです。
  • ./api/index.ts は API のメイン エントリ ポイントです。

index.ts は API のメイン エントリ ポイントです:

import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import http from 'node:http'
import https, { ServerOptions } from 'node:https'
import app from './app'
import * as databaseHelper from './common/databaseHelper'
import * as env from './config/env.config'
import * as logger from './common/logger'

if (
  await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) 
  && await databaseHelper.initialize()
) {
  let server: http.Server | https.Server

  if (env.HTTPS) {
    https.globalAgent.maxSockets = Number.POSITIVE_INFINITY
    const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8')
    const certificate = await fs.readFile(env.CERTIFICATE, 'utf8')
    const credentials: ServerOptions = { key: privateKey, cert: certificate }
    server = https.createServer(credentials, app)

    server.listen(env.PORT, () => {
      logger.info('HTTPS server is running on Port', env.PORT)
    })
  } else {
    server = app.listen(env.PORT, () => {
      logger.info('HTTP server is running on Port', env.PORT)
    })
  }

  const close = () => {
    logger.info('Gracefully stopping...')
    server.close(async () => {
      logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`)
      await databaseHelper.close(true)
      logger.info('MongoDB connection closed')
      process.exit(0)
    })
  }

  ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close))
}

これは、Node.js と Express を使用してサーバーを起動する TypeScript ファイルです。 dotenv、process、fs、http、https、mongoose、app などのいくつかのモジュールをインポートします。次に、HTTPS 環境変数が true に設定されているかどうかを確認し、true に設定されている場合は、https モジュールと提供された秘密キーと証明書を使用して HTTPS サーバーを作成します。それ以外の場合は、http モジュールを使用して HTTP サーバーを作成します。サーバーは、PORT 環境変数で指定されたポートで待機します。

close 関数は、終了信号を受信したときにサーバーを正常に停止するように定義されています。サーバーと MongoDB 接続を閉じ、ステータス コード 0 でプロセスを終了します。最後に、プロセスが SIGINT、SIGTERM、または SIGQUIT シグナルを受信したときに呼び出される close 関数を登録します。

app.ts は API のメイン エントリ ポイントです:

import express from 'express'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import cookieParser from 'cookie-parser'
import i18n from './lang/i18n'
import * as env from './config/env.config'
import cors from './middlewares/cors'
import allowedMethods from './middlewares/allowedMethods'
import agencyRoutes from './routes/agencyRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import propertyRoutes from './routes/propertyRoutes'
import userRoutes from './routes/userRoutes'
import stripeRoutes from './routes/stripeRoutes'
import countryRoutes from './routes/countryRoutes'
import * as helper from './common/helper'

const app = express()

app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())

app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))

app.use(cors())
app.options('*', cors())
app.use(cookieParser(env.COOKIE_SECRET))
app.use(allowedMethods)

app.use('/', agencyRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', propertyRoutes)
app.use('/', userRoutes)
app.use('/', stripeRoutes)
app.use('/', countryRoutes)

i18n.locale = env.DEFAULT_LANGUAGE

helper.mkdir(env.CDN_USERS)
helper.mkdir(env.CDN_TEMP_USERS)
helper.mkdir(env.CDN_PROPERTIES)
helper.mkdir(env.CDN_TEMP_PROPERTIES)
helper.mkdir(env.CDN_LOCATIONS)
helper.mkdir(env.CDN_TEMP_LOCATIONS)

export default app

まず、MongoDB 接続文字列を取得し、MongoDB データベースとの接続を確立します。次に、Express アプリを作成し、cors、compression、helmet、nocache などのミドルウェアを読み込みます。ヘルメットミドルウェアライブラリを利用して、さまざまなセキュリティ対策を設定しています。また、supplierRoutes、bookingRoutes、locationRoutes、notificationRoutes、propertyRoutes、userRoutes など、アプリケーションのさまざまな部分のさまざまなルート ファイルもインポートします。最後に、Express ルートをロードし、アプリをエクスポートします。

API には 8 つのルートがあります。各ルートには、MVC 設計パターンと SOLID 原則に従って独自のコントローラーがあります。以下は主なルートです:

  • userRoutes: ユーザーに関連する REST 機能を提供します
  • agencyRoutes: 代理店に関連する REST 機能を提供します
  • countryRoutes: 国に関連する REST 関数を提供します
  • locationRoutes: 場所に関連する REST 関数を提供します
  • propertyRoutes: プロパティに関連する REST 関数を提供します
  • bookingRoutes: 予約に関連する REST 関数を提供します
  • notificationRoutes: 通知に関連する REST 関数を提供します
  • StripeRoutes: Stripe ペイメントゲートウェイに関連する REST 機能を提供します

各ルートを一つずつ説明するつもりはありません。たとえば、propertyRoutes を取り上げ、それがどのように作成されたかを見てみましょう。ソース コードを参照して、すべてのルートを確認できます。

ここに propertyRoutes.ts があります:

import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import http from 'node:http'
import https, { ServerOptions } from 'node:https'
import app from './app'
import * as databaseHelper from './common/databaseHelper'
import * as env from './config/env.config'
import * as logger from './common/logger'

if (
  await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) 
  && await databaseHelper.initialize()
) {
  let server: http.Server | https.Server

  if (env.HTTPS) {
    https.globalAgent.maxSockets = Number.POSITIVE_INFINITY
    const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8')
    const certificate = await fs.readFile(env.CERTIFICATE, 'utf8')
    const credentials: ServerOptions = { key: privateKey, cert: certificate }
    server = https.createServer(credentials, app)

    server.listen(env.PORT, () => {
      logger.info('HTTPS server is running on Port', env.PORT)
    })
  } else {
    server = app.listen(env.PORT, () => {
      logger.info('HTTP server is running on Port', env.PORT)
    })
  }

  const close = () => {
    logger.info('Gracefully stopping...')
    server.close(async () => {
      logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`)
      await databaseHelper.close(true)
      logger.info('MongoDB connection closed')
      process.exit(0)
    })
  }

  ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close))
}

まず、Express Router を作成します。次に、名前、メソッド、ミドルウェア、コントローラーを使用してルートを作成します。

routeNames には propertyRoutes ルート名が含まれています:

import express from 'express'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import cookieParser from 'cookie-parser'
import i18n from './lang/i18n'
import * as env from './config/env.config'
import cors from './middlewares/cors'
import allowedMethods from './middlewares/allowedMethods'
import agencyRoutes from './routes/agencyRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import propertyRoutes from './routes/propertyRoutes'
import userRoutes from './routes/userRoutes'
import stripeRoutes from './routes/stripeRoutes'
import countryRoutes from './routes/countryRoutes'
import * as helper from './common/helper'

const app = express()

app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())

app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))

app.use(cors())
app.options('*', cors())
app.use(cookieParser(env.COOKIE_SECRET))
app.use(allowedMethods)

app.use('/', agencyRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', propertyRoutes)
app.use('/', userRoutes)
app.use('/', stripeRoutes)
app.use('/', countryRoutes)

i18n.locale = env.DEFAULT_LANGUAGE

helper.mkdir(env.CDN_USERS)
helper.mkdir(env.CDN_TEMP_USERS)
helper.mkdir(env.CDN_PROPERTIES)
helper.mkdir(env.CDN_TEMP_PROPERTIES)
helper.mkdir(env.CDN_LOCATIONS)
helper.mkdir(env.CDN_TEMP_LOCATIONS)

export default app

propertyController には、場所に関する主要なビジネス ロジックが含まれています。コントローラーのソース コードは非常に大きいため、すべてを見ることはできませんが、コントローラー関数の作成を例に説明します。

以下はプロパティ モデルです:

import express from 'express'
import multer from 'multer'
import routeNames from '../config/propertyRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as propertyController from '../controllers/propertyController'

const routes = express.Router()

routes.route(routeNames.create).post(authJwt.verifyToken, propertyController.create)
routes.route(routeNames.update).put(authJwt.verifyToken, propertyController.update)
routes.route(routeNames.checkProperty).get(authJwt.verifyToken, propertyController.checkProperty)
routes.route(routeNames.delete).delete(authJwt.verifyToken, propertyController.deleteProperty)
routes.route(routeNames.uploadImage).post([authJwt.verifyToken, multer({ storage: multer.memoryStorage() }).single('image')], propertyController.uploadImage)
routes.route(routeNames.deleteImage).post(authJwt.verifyToken, propertyController.deleteImage)
routes.route(routeNames.deleteTempImage).post(authJwt.verifyToken, propertyController.deleteTempImage)
routes.route(routeNames.getProperty).get(propertyController.getProperty)
routes.route(routeNames.getProperties).post(authJwt.verifyToken, propertyController.getProperties)
routes.route(routeNames.getBookingProperties).post(authJwt.verifyToken, propertyController.getBookingProperties)
routes.route(routeNames.getFrontendProperties).post(propertyController.getFrontendProperties)

export default routes

以下はプロパティタイプです:

const routes = {
  create: '/api/create-property',
  update: '/api/update-property',
  delete: '/api/delete-property/:id',
  uploadImage: '/api/upload-property-image',
  deleteTempImage: '/api/delete-temp-property-image/:fileName',
  deleteImage: '/api/delete-property-image/:property/:image',
  getProperty: '/api/property/:id/:language',
  getProperties: '/api/properties/:page/:size',
  getBookingProperties: '/api/booking-properties/:page/:size',
  getFrontendProperties: '/api/frontend-properties/:page/:size',
  checkProperty: '/api/check-property/:id',
}

export default routes

プロパティは次のもので構成されます:

  • 名前
  • A タイプ (アパート、商業、農場、住宅、工業、土地、タウンハウス)
  • 作成した代理店への参照
  • 説明
  • メイン画像
  • 追加画像
  • 寝室の数
  • バスルームの数
  • キッチンの数
  • 駐車台数
  • サイズ
  • レンタルの最低年齢
  • 場所
  • 住所 (オプション)
  • 価格
  • レンタル期間 (毎月、毎週、毎日、毎年)
  • キャンセル価格 (無料で含める場合は 0 に設定します。含めたくない場合は空白のままにするか、キャンセルの価格を設定します)
  • ペットが許可されているかどうかを示すフラグ
  • その物件が家具付きかどうかを示すフラグ
  • プロパティが非表示かどうかを示すフラグ
  • エアコンが利用可能かどうかを示すフラグ
  • その物件が賃貸可能かどうかを示すフラグ

以下はコントローラー関数の作成です:

import { Schema, model } from 'mongoose'
import * as movininTypes from ':movinin-types'
import * as env from '../config/env.config'

const propertySchema = new Schema<env.Property>(
  {
    name: {
      type: String,
      required: [true, "can't be blank"],
    },
    type: {
      type: String,
      enum: [
        movininTypes.PropertyType.House,
        movininTypes.PropertyType.Apartment,
        movininTypes.PropertyType.Townhouse,
        movininTypes.PropertyType.Plot,
        movininTypes.PropertyType.Farm,
        movininTypes.PropertyType.Commercial,
        movininTypes.PropertyType.Industrial,
      ],
      required: [true, "can't be blank"],
    },
    agency: {
      type: Schema.Types.ObjectId,
      required: [true, "can't be blank"],
      ref: 'User',
      index: true,
    },
    description: {
      type: String,
      required: [true, "can't be blank"],
    },
    available: {
      type: Boolean,
      default: true,
    },
    image: {
      type: String,
    },
    images: {
      type: [String],
    },
    bedrooms: {
      type: Number,
      required: [true, "can't be blank"],
      validate: {
        validator: Number.isInteger,
        message: '{VALUE} is not an integer value',
      },
    },
    bathrooms: {
      type: Number,
      required: [true, "can't be blank"],
      validate: {
        validator: Number.isInteger,
        message: '{VALUE} is not an integer value',
      },
    },
    kitchens: {
      type: Number,
      default: 1,
      validate: {
        validator: Number.isInteger,
        message: '{VALUE} is not an integer value',
      },
    },
    parkingSpaces: {
      type: Number,
      default: 0,
      validate: {
        validator: Number.isInteger,
        message: '{VALUE} is not an integer value',
      },
    },
    size: {
      type: Number,
    },
    petsAllowed: {
      type: Boolean,
      required: [true, "can't be blank"],
    },
    furnished: {
      type: Boolean,
      required: [true, "can't be blank"],
    },
    minimumAge: {
      type: Number,
      required: [true, "can't be blank"],
      min: env.MINIMUM_AGE,
      max: 99,
    },
    location: {
      type: Schema.Types.ObjectId,
      ref: 'Location',
      required: [true, "can't be blank"],
    },
    address: {
      type: String,
    },
    price: {
      type: Number,
      required: [true, "can't be blank"],
    },
    hidden: {
      type: Boolean,
      default: false,
    },
    cancellation: {
      type: Number,
      default: 0,
    },
    aircon: {
      type: Boolean,
      default: false,
    },
    rentalTerm: {
      type: String,
      enum: [
        movininTypes.RentalTerm.Monthly,
        movininTypes.RentalTerm.Weekly,
        movininTypes.RentalTerm.Daily,
        movininTypes.RentalTerm.Yearly,
      ],
      required: [true, "can't be blank"],
    },
  },
  {
    timestamps: true,
    strict: true,
    collection: 'Property',
  },
)

const Property = model<env.Property>('Property', propertySchema)

export default Property

フロントエンド

フロントエンドは、Node.js、React、MUI、TypeScript で構築された Web アプリケーションです。フロントエンドから、顧客は乗車場所と降車場所および時間に応じて利用可能な車を検索し、車を選択してチェックアウトに進むことができます。

  • ./frontend/src/assets/ フォルダーには CSS と画像が含まれています。
  • ./frontend/src/pages/ フォルダーには React ページが含まれています。
  • ./frontend/src/components/ フォルダーには React コンポーネントが含まれています。
  • ./frontend/src/services/ には API クライアント サービスが含まれています。
  • ./frontend/src/App.tsx は、ルートを含むメインの React アプリです。
  • ./frontend/src/index.tsx はフロントエンドのメイン エントリ ポイントです。

TypeScript の型定義は、パッケージ ./packages/movinin-types で定義されます。

App.tsx はメインの反応アプリです:

import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import http from 'node:http'
import https, { ServerOptions } from 'node:https'
import app from './app'
import * as databaseHelper from './common/databaseHelper'
import * as env from './config/env.config'
import * as logger from './common/logger'

if (
  await databaseHelper.connect(env.DB_URI, env.DB_SSL, env.DB_DEBUG) 
  && await databaseHelper.initialize()
) {
  let server: http.Server | https.Server

  if (env.HTTPS) {
    https.globalAgent.maxSockets = Number.POSITIVE_INFINITY
    const privateKey = await fs.readFile(env.PRIVATE_KEY, 'utf8')
    const certificate = await fs.readFile(env.CERTIFICATE, 'utf8')
    const credentials: ServerOptions = { key: privateKey, cert: certificate }
    server = https.createServer(credentials, app)

    server.listen(env.PORT, () => {
      logger.info('HTTPS server is running on Port', env.PORT)
    })
  } else {
    server = app.listen(env.PORT, () => {
      logger.info('HTTP server is running on Port', env.PORT)
    })
  }

  const close = () => {
    logger.info('Gracefully stopping...')
    server.close(async () => {
      logger.info(`HTTP${env.HTTPS ? 'S' : ''} server closed`)
      await databaseHelper.close(true)
      logger.info('MongoDB connection closed')
      process.exit(0)
    })
  }

  ['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((signal) => process.on(signal, close))
}

各ルートをロードするために React 遅延読み込みを使用しています。

フロントエンドの各ページについては説明しませんが、ソース コードを参照して各ページを確認することができます。

モバイルアプリ

このプラットフォームは、Android および iOS 用のネイティブ モバイル アプリを提供します。モバイル アプリは React Native、Expo、TypeScript で構築されています。フロントエンドと同様に、モバイル アプリでも、顧客は乗車場所と降車場所および時間に応じて利用可能な車を検索し、車を選択してチェックアウトに進むことができます。

顧客は、予約がバックエンドから更新されるとプッシュ通知を受け取ります。プッシュ通知は、Node.js、Expo Server SDK、Firebase を使用して構築されています。

  • ./mobile/assets/ フォルダーには画像が含まれています。
  • ./mobile/screens/ フォルダーには、React Native のメイン画面が含まれています。
  • ./mobile/components/ フォルダーには React Native コンポーネントが含まれています。
  • ./mobile/services/ には API クライアント サービスが含まれています。
  • ./mobile/App.tsx はメインの React Native アプリです。

TypeScript の型定義は次の場所で定義されます:

  • ./mobile/types/index.d.ts
  • ./mobile/types/env.d.ts
  • ./mobile/miscellaneous/movininTypes.ts

./mobile/types/ は、次のように ./mobile/tsconfig.json にロードされます。

import express from 'express'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import cookieParser from 'cookie-parser'
import i18n from './lang/i18n'
import * as env from './config/env.config'
import cors from './middlewares/cors'
import allowedMethods from './middlewares/allowedMethods'
import agencyRoutes from './routes/agencyRoutes'
import bookingRoutes from './routes/bookingRoutes'
import locationRoutes from './routes/locationRoutes'
import notificationRoutes from './routes/notificationRoutes'
import propertyRoutes from './routes/propertyRoutes'
import userRoutes from './routes/userRoutes'
import stripeRoutes from './routes/stripeRoutes'
import countryRoutes from './routes/countryRoutes'
import * as helper from './common/helper'

const app = express()

app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())

app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))

app.use(cors())
app.options('*', cors())
app.use(cookieParser(env.COOKIE_SECRET))
app.use(allowedMethods)

app.use('/', agencyRoutes)
app.use('/', bookingRoutes)
app.use('/', locationRoutes)
app.use('/', notificationRoutes)
app.use('/', propertyRoutes)
app.use('/', userRoutes)
app.use('/', stripeRoutes)
app.use('/', countryRoutes)

i18n.locale = env.DEFAULT_LANGUAGE

helper.mkdir(env.CDN_USERS)
helper.mkdir(env.CDN_TEMP_USERS)
helper.mkdir(env.CDN_PROPERTIES)
helper.mkdir(env.CDN_TEMP_PROPERTIES)
helper.mkdir(env.CDN_LOCATIONS)
helper.mkdir(env.CDN_TEMP_LOCATIONS)

export default app

App.tsx は React Native アプリのメイン エントリ ポイントです:

「react-native-gesture-handler」をインポートします
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { RootSiblingParent } from 'react-native-root-siblings'
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native'
import { StatusBar as ExpoStatusBar } from 'expo-status-bar'
import { SafeAreaProvider } から 'react-native-safe-area-context'
import { Provider } from 'react-native-paper'
import * as SplashScreen from 'expo-splash-screen'
import * 'expo-notifications' からの通知として
import { StripeProvider } から '@ストライプ/ストライプ-反応-ネイティブ'
'./components/DrawerNavigator' から DrawerNavigator をインポートします
import * './common/helper' からヘルパーとして
import * as NoticeService from './services/NotificationService'
import * as UserService from './services/UserService'
'./context/GlobalContext' から { GlobalProvider } をインポートします
import * as env from './config/env.config'

Notices.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true、
    shouldPlaySound: true、
    shouldSetBadge: true、
  })、
})

//
// アプリコンポーネントの宣言前にネイティブのスプラッシュ画面が自動非表示になるのを防ぎます
//
SplashScreen.preventAutoHideAsync()
  .then((result) => console.log(`SplashScreen.preventAutoHideAsync() が成功しました: ${result}`))
  .catch(console.warn) // エラーを明示的にキャッチして検査することは良いことです

const App = () => {
  const [appIsReady, setAppIsReady] = useState(false)

  const responseListener = useRef<Notifications.Subscription>()
  const NavigationRef = useRef<navigationcontainerref>>(null)

  useEffect(() => {
    const register = async () => {
      const loggedIn = await UserService.loggedIn()
      if (ログイン) {
        const currentUser = await UserService.getCurrentUser()
        if (currentUser?._id) {
          await helper.registerPushToken(currentUser._id)
        } それ以外 {
          helper.error()
        }
      }
    }

    //
    // プッシュ通知トークンを登録する
    //
    登録する()

    //
    // このリスナーは、ユーザーが通知をタップするか通知を操作するたびに起動されます (アプリがフォアグラウンド、バックグラウンド、または強制終了されたときに機能します)
    //
    responseListener.current = Notices.addNotificationResponseReceivedListener(async (応答) => {
      試す {
        if (navigationRef.current) {
          const { データ } = 応答.通知.リクエスト.コンテンツ

          if (data.booking) {
            if (data.user && data.notification) {
              await NoticeService.markAsRead(data.user, [data.notification])
            }
            NavigationRef.current.navigate('予約', { id: data.booking })
          } それ以外 {
            NavigationRef.current.navigate('通知', {})
          }
        }
      } キャッチ (エラー) {
        helper.error(エラー、偽)
      }
    })

    return() => {
      Notices.removeNotificationSubscription(responseListener.current!)
    }
  }、[])

  setTimeout(() => {
    setAppIsReady(true)
  }、500)

  const onReady = useCallback(async () => {
    if (appIsReady) {
      //
      // これは、スプラッシュ スクリーンをすぐに非表示にするように指示します。これを後で呼び出すと
      // `setAppIsReady` の場合、アプリの起動中に空白の画面が表示される場合があります。
      // 初期状態をロードし、最初のピクセルをレンダリングします。そこで代わりに、
      // ルート ビューがすでに存在していることがわかったら、スプラッシュ スクリーンを非表示にします。
      // レイアウトを実行しました。
      //
      SplashScreen.hideAsync() を待つ
    }
  }, [appIsReady])

  if (!appIsReady) {
    nullを返す
  }

  戻る (
    <グローバルプロバイダー>
      <セーフエリアプロバイダー>
        <プロバイダ>
          <StripeProvider publicableKey={env.STRIPE_PUBLISHABLE_KEY} MerchantIdentifier={env.STRIPE_MERCHANT_IDENTIFIER}>
            <RootSiblingParent>
              <NavigationContainer ref={navigationRef} onReady={onReady}>
                <expostatusbar>



<p>モバイル アプリの各画面については説明しませんが、ソース コードを参照して各画面を確認することができます。</p>

<h2>
  
  
  管理者ダッシュボード
</h2>

<p>管理者ダッシュボードは、Node.js、React、MUI、TypeScript で構築された Web アプリケーションです。管理者はバックエンドから、サプライヤー、車両、場所、顧客、予約を作成および管理できます。新しいサプライヤーがバックエンドから作成されると、管理ダッシュボードにアクセスして車両の保有車両と予約を管理するためにアカウントを作成するよう求めるメールが送信されます。</p>

<ul>
<li>./backend/assets/ フォルダーには CSS と画像が含まれています。</li>
<li>./backend/pages/ フォルダーには React ページが含まれています。</li>
<li>./backend/components/ フォルダーには React コンポーネントが含まれています。</li>
<li>./backend/services/ には API クライアント サービスが含まれています。</li>
<li>./backend/App.tsx は、ルートを含むメインの React アプリです。</li>
<li>./backend/index.tsx は、管理ダッシュボードのメイン エントリ ポイントです。</li>
</ul>

<p>TypeScript の型定義は、パッケージ ./packages/movinin-types で定義されます。</p>

<p>管理ダッシュボードの App.tsx は、フロントエンドの App.tsx と同様のロジックに従います。</p>

<p>管理者ダッシュボードの各ページについては説明しませんが、ソース コードを参照して各ページを確認することができます。</p>

<h2>
  
  
  興味のあるスポット
</h2>

<p>React Native と Expo を使用してモバイル アプリを構築するのは非常に簡単です。 Expo では、React Native を使用したモバイル開発が非常に簡単になります。</p>

<p>バックエンド、フロントエンド、モバイル開発に同じ言語 (TypeScript) を使用すると、非常に便利です。</p>

<p>TypeScript は非常に興味深い言語であり、多くの利点があります。 JavaScript に静的型付けを追加することで、多くのバグを回避し、デバッグとテストが容易な、高品質でスケーラブルで読みやすく保守しやすいコードを生成できます。</p>

<p>それだけです!この記事を楽しんで読んでいただければ幸いです。</p>
<h2>
  
  
  リソース
</h2>

<ol>
<li>概要</li>
<li>建築</li>
<li>インストール中 (セルフホスト型)</li>
<li>インストール(VPS)</li>
<li>
インストール(Docker)

<ol>
<li>Docker イメージ</li>
<li>SSL</li>
</ol>


</li>

<li>ストライプのセットアップ</li>

<li>モバイルアプリを構築</li>

<li>

デモデータベース

<ol>
<li>Windows、Linux、macOS</li>
<li>ドッカー</li>
</ol>


</li>

<li>ソースから実行</li>

<li>

モバイルアプリを実行する

<ol>
<li>前提条件</li>
<li>手順</li>
<li>プッシュ通知</li>
</ol>


</li>

<li>通貨を変更</li>

<li>新しい言語を追加</li>

<li>単体テストとカバレッジ</li>

<li>ログ</li>

</ol>


          

            
        </expostatusbar></navigationcontainerref>

以上がゼロからヒーローへ: 不動産賃貸ウェブサイトとモバイル アプリを構築する私の旅の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。