Spring BootとVue.jsで構築する不動産賃貸管理システムの実装ガイド

システム概要

本システムは、Spring Bootをバックエンド、Vue.jsをフロントエンドとして実装した不動産賃貸管理プラットフォームです。UniAppを活用することで、Webアプリケーションとモバイルアプリの両方に対応した統一的なユーザー体験を提供します。

技術アーキテクチャ

バックエンド:Spring Bootフレームワーク

Spring Bootは、自動設定機能により開発効率を大幅に向上させます。内蔵サーバーにより、追加設定なしでアプリケーションを即座に実行できます。Spring Data JPAやSpring Securityとの統合により、堅牢なエンタープライズアプリケーションを迅速に構築可能です。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll()
            .antMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

フロントエンド:Vue.jsエコシステム

Vue.jsのリアクティブデータバインディングとコンポーネントベースのアーキテクチャにより、インタラクティブなUIを効率的に実装できます。Vuexを使用した状態管理とVue Routerによるルーティング機能で、スケーラブルなシングルページアプリケーションを構築します。

<template>
  <div class="property-list">
    <div v-for="property in filteredProperties" 
         :key="property.id" 
         class="property-card"
         @click="showDetails(property)">
      <img :src="property.image" :alt="property.title">
      <h3>{{ property.title }}</h3>
      <p>¥{{ formatPrice(property.price) }}/月</p>
      <div class="property-features">
        <span v-for="feature in property.features" :key="feature">
          {{ feature }}
        </span>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'PropertyList',
  data() {
    return {
      properties: [],
      filters: {
        priceRange: [0, 100000],
        location: '',
        propertyType: 'all'
      }
    }
  },
  computed: {
    filteredProperties() {
      return this.properties.filter(property => {
        return property.price >= this.filters.priceRange[0] && 
               property.price <= this.filters.priceRange[1] &&
               (this.filters.location === '' || property.location.includes(this.filters.location))
      })
    }
  },
  methods: {
    async fetchProperties() {
      try {
        const response = await this.$http.get('/api/properties')
        this.properties = response.data
      } catch (error) {
        console.error('物件データの取得に失敗しました:', error)
      }
    },
    formatPrice(price) {
      return price.toLocaleString('ja-JP')
    },
    showDetails(property) {
      this.$router.push(`/properties/${property.id}`)
    }
  },
  mounted() {
    this.fetchProperties()
  }
}
</script>

データベース設計:MySQLとMyBatis

MyBatisを使用することで、SQL文をJavaコードから分離し、柔軟なデータアクセス層を実現します。XMLマッピングファイルにより、複雑なクエリも簡単に管理できます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.rental.mapper.PropertyMapper">
  
  <resultMap id="PropertyResultMap" type="com.rental.entity.Property">
    <id property="id" column="id"/>
    <result property="title" column="title"/>
    <result property="description" column="description"/>
    <result property="price" column="price"/>
    <result property="address" column="address"/>
    <result property="area" column="area"/>
    <result property="rooms" column="rooms"/>
    <result property="status" column="status"/>
    <association property="owner" javaType="com.rental.entity.User">
      <id property="id" column="owner_id"/>
      <result property="name" column="owner_name"/>
      <result property="phone" column="owner_phone"/>
    </association>
  </resultMap>
  
  <select id="findByFilters" resultMap="PropertyResultMap">
    SELECT p.*, u.name as owner_name, u.phone as owner_phone
    FROM properties p
    LEFT JOIN users u ON p.owner_id = u.id
    WHERE 1=1
    <if test="minPrice != null">
      AND p.price >= #{minPrice}
    </if>
    <if test="maxPrice != null">
      AND p.price <= #{maxPrice}
    </if>
    <if test="location != null and location != ''">
      AND p.address LIKE CONCAT('%', #{location}, '%')
    </if>
    <if test="propertyType != null and propertyType != ''">
      AND p.property_type = #{propertyType}
    </if>
    ORDER BY p.created_at DESC
  </select>
  
</mapper>

主要機能モジュール

物件管理機能

物件の登録、編集、削除、検索機能を実装。画像アップロード、詳細説明、価格設定、施設情報の管理が可能です。

予約管理システム

予約カレンダー、空室状況のリアルタイム更新、予約申請の承認・拒否機能を提供します。

@RestController
@RequestMapping("/api/bookings")
public class BookingController {
    
    @Autowired
    private BookingService bookingService;
    
    @PostMapping("/create")
    public ResponseEntity<ApiResponse> createBooking(
            @RequestBody BookingRequest request,
            @AuthenticationPrincipal UserDetails userDetails) {
        
        try {
            Booking booking = bookingService.createBooking(
                request.getPropertyId(),
                userDetails.getUsername(),
                request.getCheckInDate(),
                request.getCheckOutDate()
            );
            return ResponseEntity.ok(new ApiResponse("予約が正常に作成されました", booking));
        } catch (PropertyNotAvailableException e) {
            return ResponseEntity.badRequest()
                .body(new ApiResponse("指定の日付は予約できません", null));
        }
    }
    
    @GetMapping("/my-bookings")
    public ResponseEntity<List<Booking>> getUserBookings(
            @AuthenticationPrincipal UserDetails userDetails) {
        List<Booking> bookings = bookingService.getUserBookings(userDetails.getUsername());
        return ResponseEntity.ok(bookings);
    }
}

決済連携

外部決済サービス(StripeやSquare)との連携により、安全なオンライン決済を実現します。

システムテスト戦略

単体テスト

JUnit 5とMockitoを使用して、各サービス層のビジネスロジックを網羅的にテストします。

@ExtendWith(MockitoExtension.class)
class PropertyServiceTest {
    
    @Mock
    private PropertyRepository propertyRepository;
    
    @InjectMocks
    private PropertyService propertyService;
    
    @Test
    void shouldReturnAvailableProperties() {
        // Given
        Property property1 = new Property(1L, "物件A", 50000, "東京");
        Property property2 = new Property(2L, "物件B", 70000, "大阪");
        List<Property> expectedProperties = Arrays.asList(property1, property2);
        
        when(propertyRepository.findByStatus("AVAILABLE")).thenReturn(expectedProperties);
        
        // When
        List<Property> actualProperties = propertyService.getAvailableProperties();
        
        // Then
        assertEquals(expectedProperties.size(), actualProperties.size());
        assertEquals(expectedProperties, actualProperties);
        verify(propertyRepository, times(1)).findByStatus("AVAILABLE");
    }
}

統合テスト

Spring Boot Testを使用して、コントローラーからデータベースまでの全体フローをテストします。

E2Eテスト

CypressまたはSeleniumを使用して、ユーザーの操作シナリオを再現し、UIの正確性を検証します。

デプロイメント構成

Dockerコンテナ化

FROM openjdk:11-jre-slim
COPY target/rental-system.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

Kubernetesマニフェスト

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rental-backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: rental-backend
  template:
    metadata:
      labels:
        app: rental-backend
    spec:
      containers:
      - name: backend
        image: rental-system:latest
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "production"
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: rental-config
              key: db.host

データベーススキーマ

-- 物件テーブル
CREATE TABLE properties (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  title VARCHAR(200) NOT NULL,
  description TEXT,
  price DECIMAL(10,2) NOT NULL,
  address VARCHAR(500) NOT NULL,
  area DECIMAL(8,2),
  rooms INT,
  property_type ENUM('APARTMENT', 'HOUSE', 'STUDIO') DEFAULT 'APARTMENT',
  status ENUM('AVAILABLE', 'RENTED', 'MAINTENANCE') DEFAULT 'AVAILABLE',
  owner_id BIGINT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (owner_id) REFERENCES users(id)
);

-- 予約テーブル
CREATE TABLE bookings (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  property_id BIGINT NOT NULL,
  tenant_id BIGINT NOT NULL,
  check_in_date DATE NOT NULL,
  check_out_date DATE NOT NULL,
  total_amount DECIMAL(10,2) NOT NULL,
  status ENUM('PENDING', 'CONFIRMED', 'CANCELLED') DEFAULT 'PENDING',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (property_id) REFERENCES properties(id),
  FOREIGN KEY (tenant_id) REFERENCES users(id)
);

タグ: SpringBoot vue.js UniApp MySQL MyBatis

5月14日 11:51 投稿