4
4

More than 1 year has passed since last update.

【SpringBoot】図書の貸出管理システムを作りたい

Last updated at Posted at 2023-07-25

はじめに

前回までの内容で図書管理システムの設計しログイン機能を実装したので、それにChatGPTに質問しながらEclipseを使って開発をした流れを記事にしました

システム実装に必要なリソース

今回は図書の貸出登録機能を実装するため以下が必要になります

追加するテーブル

  • 図書の情報(Booksテーブル)
  • 貸出ユーザー管理(BorrowedBooksテーブル)
  • 貸出履歴管理(BorrowHistoryテーブル)

追加する機能

  • ブラウザで図書の一覧表示
  • 図書の検索機能
  • 貸出ボタン/返却ボタン
  • 図書詳細閲覧

一覧表示機能の実装(Booksテーブルの実装)

ファイル構造図(Booksクラス)

追加予定図
src/
  main/
    java/
      com/
        example/
          librarymanagementsystem/
            model/
              Book.java
            repository/
              BooksRepository.java
            service/
              BooksService.java
            controller/
              BookController.java

データベースへの接続(MySQL)

ターミナルもしくはコマンドラインでデータベースに接続します

データベース接続
mysql -u username -p

パスワードが求められるためパスワードを入力します

すでに作成しているデータベースを選択します

データベース名がLibraryManagementSystemの場合
USE LibraryManagementSystem;

Booksテーブルの作成

MySQLの場合は次のSQLを実行します

mysql> ここに以下のコマンドを入力する



CREATE TABLE Books (
    Id INT PRIMARY KEY,
    title VARCHAR(255),
    author VARCHAR(255),
    publication_Year INT,
    publisher VARCHAR(255),
    genre VARCHAR(255),
    status ENUM('available', 'borrowed') NOT NULL
);

テーブル作成(Books)

テーブル名:Books

名前 タイプ
Id int(11)
title varchar(255)
author varchar(255)
publication_Year varchar(255)
publisher varchar(255)
genre varchar(255)
status ENUM('available', 'borrowed') NOT NULL

テーブルの自動生成

エンティティクラスを作成しSpringBootのアプリを実行すると、接続されているデータベースに対してテーブルが自動生成されます(接続されていなければ自動生成はできません)

自動生成するためには次のどちらかのファイルに自動生成をするための設定が必要です

application.properties
spring.jpa.hibernate.ddl-auto=update
application.yml
spring:
  jpa:
    hibernate:
      ddl-auto: update

ddl-autoオプションには以下の値を設定することができます:

  • none : 何も行いません。
  • validate : テーブルがエンティティと一致しているかチェックします。
  • update : テーブルが存在しない場合は生成し、エンティティとテーブルの間の差分があれば更新します。
  • create : 既存のテーブルを削除し新たに生成します。
  • create-drop : セッションが終了すると生成したテーブルを削除します。
    これらのオプションは、開発・テスト・プロダクション環境によって適切に選ぶ必要があります。例えば、createやcreate-dropはデータが消えるため、プロダクション環境で使用するべきではありません。

エンティティクラス(モデルクラス)の作成

データベースのテーブル(カラム)とJavaの変数(フィールド)を紐付けるクラスであり、テーブルの値を保持するためのクラスです。このクラスは、各変数(フィールド)がテーブルのカラムと対応しており、変数にはテーブルの値が格納されます。モデルクラスとも言います

Book.java
// この部分は、このコードが何処にあるのか(どのフォルダにあるのか)を示しています。"com.example.model"というのは、このコードがどこにあるかを示す場所のようなものです。
package com.example.model;

// このコードで使う機能をここで読み込んでいます。これによって、このコードの中で、それらの機能を自由に使えるようになります。
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

// @Entityは、このクラスがデータベースのテーブルに対応することを示しています。
@Entity
// @Tableは、このクラスがどのテーブルに対応するかを指定しています。ここでは、"books"というテーブルに対応していると宣言しています。
@Table(name="books")
// @Getterと@Setterは、このクラスの全てのフィールド(この下に書かれているid、titleなど)について、自動的に取得や設定のためのコードを作成することを指示します。これにより、別のコードからこれらの値を取得したり設定したりすることが可能になります。
@Getter
@Setter
public class Book {
    // @Idは、このフィールドがテーブルの主キー(ユニークなID)であることを示しています。
    @Id
    private int id;

    // 以下は、本の情報を保存するためのフィールド(変数)です。タイトル、著者、出版年、出版社、ジャンル、状態を保存します。
    private String title;
    private String author;
    private int publicationYear;
    private String publisher;
    private String genre;
    private String status;

    // @ManyToOneと@JoinColumnは、本が誰に借りられているかを示すためのフィールドを作成します。Userというクラスのオブジェクト(つまり、借りているユーザー)を保存します。この本が借りられていない場合は、このフィールドは空(null)になります。
    @ManyToOne
    @JoinColumn(name = "borrowed_user_id")
    private User borrowedUser;
}

BookHistory.java
// この部分は、このコードが何処にあるのか(どのフォルダにあるのか)を示しています。"com.example.model"というのは、このコードがどこにあるかを示す場所のようなものです。
package com.example.model;

// このコードで使う機能をここで読み込んでいます。これによって、このコードの中で、それらの機能を自由に使えるようになります。
import java.time.LocalDate;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

import lombok.Getter;
import lombok.Setter;

// @Entityは、このクラスがデータベースのテーブルに対応することを示しています。
@Entity
// @Getterと@Setterは、このクラスの全てのフィールド(この下に書かれているid、bookなど)について、自動的に取得や設定のためのコードを作成することを指示します。これにより、別のコードからこれらの値を取得したり設定したりすることが可能になります。
@Getter
@Setter
public class BookHistory {
    // @Idは、このフィールドがテーブルの主キー(ユニークなID)であることを示しています。
    // @GeneratedValueは、このIDが自動的に生成されることを示しています。
    @Id
    @GeneratedValue
    private int id;

    // この履歴エントリがどの本に関連するのかを保存するためのフィールドです。
    @ManyToOne
    private Book book;

    // この履歴エントリがどのユーザーに関連するのかを保存するためのフィールドです。
    @ManyToOne
    private User user;

    // 本が借りられた日と返却された日を保存するためのフィールドです。
    private LocalDate borrowedDate;
    private LocalDate returnedDate;

    // 行動(貸出か返却)を保存するためのフィールドです。
    private String action;

    // この下には、フィールドの値を取得したり設定したりするためのコードが入ります(getters and setters)。
}

BorrowedBook.java
// この部分は、このコードが何処にあるのか(どのフォルダにあるのか)を示しています。"com.example.model"というのは、このコードがどこにあるかを示す場所のようなものです。
package com.example.model;

// このコードで使う機能をここで読み込んでいます。これによって、このコードの中で、それらの機能を自由に使えるようになります。
import java.time.LocalDate;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

// @Entityは、このクラスがデータベースのテーブルに対応することを示しています。
@Entity
// @Table(name="borrowed_books")は、このクラスがどのテーブルに対応するかを示しています。この場合、"borrowed_books"という名前のテーブルに対応します。
@Table(name="borrowed_books")
// @Getterと@Setterは、このクラスの全てのフィールド(この下に書かれているid、bookなど)について、自動的に取得や設定のためのコードを作成することを指示します。これにより、別のコードからこれらの値を取得したり設定したりすることが可能になります。
@Getter
@Setter
public class BorrowedBook {
    // @Idは、このフィールドがテーブルの主キー(ユニークなID)であることを示しています。
    // @GeneratedValue(strategy = GenerationType.AUTO)は、このIDが自動的に生成されることを示しています。
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    // この借りた本がどの本に関連するのかを保存するためのフィールドです。@JoinColumn(name="book_Id", nullable=false)は、この値が必ず存在すること、そしてデータベーステーブルでの列名が"book_Id"であることを示しています。
    @ManyToOne
    @JoinColumn(name="book_Id", nullable=false)
    private Book book;

    // この借りた本がどのユーザーに関連するのかを保存するためのフィールドです。@JoinColumn(name="user_Id", nullable=false)は、この値が必ず存在すること、そしてデータベーステーブルでの列名が"user_Id"であることを示しています。
    @ManyToOne
    @JoinColumn(name="user_Id", nullable=false)
    private User user;

    // 本が借りられた日と返却された日を保存するためのフィールドです。
    private LocalDate borrowedDate;
    private LocalDate returnDate;
}

User.java
// この部分は、このコードが何処にあるのか(どのフォルダにあるのか)を示しています。"com.example.model"というのは、このコードがどこにあるかを示す場所のようなものです。
package com.example.model;

// このコードで使う機能をここで読み込んでいます。これによって、このコードの中で、それらの機能を自由に使えるようになります。
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

// @Entityは、このクラスがデータベースのテーブルに対応することを示しています。
@Entity
// @Table(name = "Users")は、このクラスがどのテーブルに対応するかを示しています。この場合、"Users"という名前のテーブルに対応します。
@Table(name = "Users")
public class User {
    // 以下のフィールド(id、usernameなど)は、このユーザークラスの一部を形成します。

    // @Idは、このフィールドがテーブルの主キー(ユニークなID)であることを示しています。
    // @GeneratedValue(strategy = GenerationType.IDENTITY)は、このIDが自動的に生成されることを示しています。
    // @Column(name = "id")は、データベーステーブルでの列名が"id"であることを示しています。
    // @Getterと@Setterは、このフィールド(id)を取得したり(get)、設定したり(set)するための方法を自動的に作り出すことを示しています。
    @Id
    @Getter
    @Setter
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    // @Column(name = "username", nullable = false, unique = true)は、データベーステーブルでの列名が"username"であり、この値が必ず存在し、かつユニーク(他のユーザーと同じ名前がない)であることを示しています。
    // @Getterと@Setterは、このフィールド(username)を取得したり(get)、設定したり(set)するための方法を自動的に作り出すことを示しています。
    @Getter
    @Setter
    @Column(name = "username", nullable = false, unique = true)
    private String username;

    // @Column(name = "password", nullable = false)は、データベーステーブルでの列名が"password"であり、この値が必ず存在することを示しています。
    // @Getterと@Setterは、このフィールド(password)を取得したり(get)、設定したり(set)するための方法を自動的に作り出すことを示しています。
    @Getter
    @Setter
    @Column(name = "password", nullable = false)
    private String password;

    // @Column(name = "email", nullable = false, unique = true)は、データベーステーブルでの列名が"email"であり、この値が必ず存在し、かつユニーク(他のユーザーと同じメールアドレスがない)であることを示しています。
    // @Getterと@Setterは、このフィールド(email)を取得したり(get)、設定したり(set)するための方法を自動的に作り出すことを示しています。
    @Getter
    @Setter
    @Column(name = "email", nullable = false, unique = true)
    private String email;
}

UserDto.java
package com.example.model;

import javax.validation.constraints.NotEmpty;

import lombok.Getter;
import lombok.Setter;

public class UserDto {
    
    // 「@Getter」と「@Setter」は、このフィールドのための「get」メソッド(値を取得する)と「set」メソッド(値を設定する)を自動的に作成します。
    // 「@NotEmpty」は、このフィールドが空でないことを保証します。つまり、ユーザ名は必ず何かしらの値を持つことが必要です。
    @Getter
    @Setter
    @NotEmpty
    private String username;

    // 同様に、「@Getter」と「@Setter」はパスワードのための「get」メソッドと「set」メソッドを作ります。「@NotEmpty」は、パスワードが必ず存在することを保証します。
    @Getter
    @Setter
    @NotEmpty
    private String password;

    // メールアドレスのための「get」メソッドと「set」メソッドを作り、メールアドレスが必ず存在することを保証します。
    @Getter
    @Setter
    @NotEmpty
    private String email;
}


リポジトリークラス

データを保存したり、取得したりするための部品のことです。例えば本の情報を保存したいときリポジトリークラスがその役割を果たします。その本の情報を取得する時もリポジトリークラスに記述した操作方法で情報を取り出すことができます。これによりデータの保存や取得方法がどこでどのように行われているかを他の部分が気にすることなく必要なデータにアクセスできます。

BooksRepository.java
package com.example.repository;

import org.springframework.data.repository.CrudRepository;

import com.example.model.Book;

// このコードは、"Book"という名前のものをデータベースに保存したり、取り出したりするための「作業指示」をまとめたもの(インターフェース)を作っています。
// "CrudRepository"という部分は、データベースにデータを作成したり、取り出したり、更新したり、削除したりするための基本的な作業指示をまとめたものを利用しています。
public interface BooksRepository extends CrudRepository<Book, Integer> {
    // ここには特に作業指示が書かれていませんが、"CrudRepository"から受け継いだ作業指示を使うことができます。
    // "Book"という部分は、この「作業指示」がどの種類のデータ(この場合は「Book」)に対して働くかを示しています。
    // "Integer"という部分は、「Book」の各項目を一意に識別するための「ID」が整数(Integer)であることを示しています。
}

UserRepository.java
package com.example.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.model.User;

// UserRepositoryはデータベースへの操作を定義する場所(インターフェース)です。Userという種類のデータに対して、データベースからデータを取り出したり、データベースにデータを保存したり、データを更新したり、データを削除したりするための操作を定義します。
public interface UserRepository extends JpaRepository<User, Integer> {

    // "findByUsername"という操作は、与えられたユーザー名を持つUserを探し出して返します。例えば、"findByUsername("bob")"とすると、ユーザー名が"bob"のUserをデータベースから探し出して返します。この操作は、データベースに問い合わせを行い、その結果をUserとして返すことを意味します。
    User findByUsername(String username);
}

BorrowedBookRepository.java
package com.example.repository;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

import com.example.model.Book;
import com.example.model.BorrowedBook;

// このコードは、「BorrowedBook」(借りた本)に関するデータをデータベースから取得したり保存したりするための「仕事の手順」を定義しています。
public interface BorrowedBookRepository extends CrudRepository<BorrowedBook, Long> {
    // この部分は、特定の本を指定して、それが誰によって借りられたかのリストを取得するための「仕事の手順」を定義しています。
    List<BorrowedBook> findByBook(Book book);
}

BookHistoryRepository.java
package com.example.repository;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

import com.example.model.Book;
import com.example.model.BookHistory;
import com.example.model.User;

// このコードはデータベースのBookHistoryテーブルとのやりとりを管理するインターフェースを定義しています。
// CrudRepositoryを拡張していますので、データベースに対する基本的な操作(作成、読み込み、更新、削除など)をこのインターフェースを通して行うことができます。
public interface BookHistoryRepository extends CrudRepository<BookHistory, Integer> {

    // findByBookAndUserというメソッドは、特定の本(Book)とユーザー(User)による本の履歴(BookHistory)のリストをデータベースから探し出すために使います。
    // このメソッド名は特殊で、Spring Data JPAが自動的にこのメソッドの実装を提供します。
    // BookとUserオブジェクトを引数に渡すと、それにマッチする本の履歴のリストを返してくれます。
    List<BookHistory> findByBookAndUser(Book book, User user);
}

サービスクラス

アプリケーションで何か特定の仕事をする部分のことを指します。たとえばユーザーの情報を更新したり、本を借りたり返したりするなどの具体的な作業をこのクラスが行います。サービスクラスは、通常、他の部品(例えばリポジトリークラス)を使ってデータにアクセスし、そのデータを使って何か作業を行います。それぞれのサービスクラスは自分自身の特定の仕事に集中し、それ以外の仕事については他のクラスに任せることで、アプリケーションを整理しやすくします。

BookService.java
package com.example.service;

import java.time.LocalDate;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.model.Book;
import com.example.model.BookHistory;
import com.example.model.BorrowedBook;
import com.example.model.User;
import com.example.repository.BookHistoryRepository;
import com.example.repository.BooksRepository;
import com.example.repository.BorrowedBookRepository;
import com.example.repository.UserRepository;

// このクラスは、本に関する操作を行うためのものです
@Service
public class BookService {

    // 以下の四つは、本やユーザー、借りた本、本の履歴といったデータを操作するためのツールです
    @Autowired
    private BooksRepository bookRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BorrowedBookRepository borrowedBookRepository;

    @Autowired
    private BookHistoryRepository bookHistoryRepository;

    // このメソッドは、指定されたユーザーが指定された本を借りるときに使います
    public void borrowBook(int bookId, int userId) {
        // ユーザーと本をデータから探し出します
        User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("Invalid user Id:" + userId));
        Book book = bookRepository.findById(bookId).orElseThrow(() -> new IllegalArgumentException("Invalid book Id:" + bookId));

        // 本の状態を「借りられた」に変更して、データに保存します
        book.setStatus("borrowed");
        bookRepository.save(book);

        // 「借りた本」のデータを作って保存します
        BorrowedBook borrowedBook = new BorrowedBook();
        borrowedBook.setBook(book);
        borrowedBook.setUser(user);
        borrowedBook.setBorrowedDate(LocalDate.now());
        borrowedBookRepository.save(borrowedBook);

        // 「本の履歴」のデータを作って保存します
        BookHistory history = new BookHistory();
        history.setBook(book);
        history.setUser(user);
        history.setBorrowedDate(LocalDate.now());
        history.setAction("borrow"); // 操作を「借りる」に設定
        bookHistoryRepository.save(history);
    }

    // このメソッドは、指定された本が返されたときに使います
    public void returnBook(int bookId) {
        // 本と「借りた本」のデータを探し出します
        Book book = bookRepository.findById(bookId).orElseThrow(() -> new IllegalArgumentException("Invalid book Id:" + bookId));
        BorrowedBook borrowedBook = borrowedBookRepository.findByBook(book).stream().findFirst().orElseThrow(() -> new IllegalArgumentException("No borrowed record found for book Id:" + bookId));

        // 本の状態を「利用可能」に変更して、データに保存します
        book.setStatus("available");
        bookRepository.save(book);

        // 「借りた本」のデータを削除します
        borrowedBookRepository.delete(borrowedBook);

        // 「本の履歴」のデータを作って保存します
        BookHistory history = new BookHistory();
        history.setBook(book);
        history.setUser(borrowedBook.getUser()); // 本は借りたユーザーが返しました
        history.setReturnedDate(LocalDate.now());
        history.setAction("return"); // 操作を「返す」に設定
        bookHistoryRepository.save(history);
    }

    // このメソッドは、指定されたユーザーが指定された本の状態を切り替えるとき(借りるか返すか)に使います
    public void toggleBookStatus(int bookId, int userId) {
        // 本をデータから探し出します
        Book book = bookRepository.findById(bookId).orElseThrow(() -> new IllegalArgumentException("Invalid book Id:" + bookId));

        // 本の状態によって、「借りる」メソッドか「返す」メソッドを実行します
        if ("available".equals(book.getStatus())) {
            borrowBook(bookId, userId);
        } else if ("borrowed".equals(book.getStatus())) {
            returnBook(bookId);
        }
    }
}


UserPrincipal.java
package com.example.service;

import java.util.Collection;
import java.util.Collections;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.example.model.User;

// このクラスは、ユーザーの詳細情報を保持して、Spring Securityで利用します。
public class UserPrincipal implements UserDetails {

    private User user; // ユーザー情報

    // ユーザー情報を受け取ってセットします
    public UserPrincipal(User user) {
        this.user = user;
    }

    // このユーザーに与えられている権限(アクセスできる機能等)を返します。この例では、全てのユーザーに"USER"という権限を与えています
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority("USER"));
    }

    // ユーザーのパスワードを返します
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    // ユーザーのユーザーネームを返します
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // アカウントが有効期限内であるかを返します。ここでは常にtrue(有効)を返します
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // アカウントがロックされていないかを返します。ここでは常にtrue(ロックされていない)を返します
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 資格情報(パスワードなど)が有効期限内であるかを返します。ここでは常にtrue(有効)を返します
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // アカウントが有効(使用可能)であるかを返します。ここでは常にtrue(有効)を返します
    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserService.java
package com.example.service;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import com.example.model.User;
import com.example.model.UserDto;
import com.example.repository.UserRepository;

// このクラスは、ユーザーに関する機能を提供します。ログインのためのユーザー検索や新規ユーザーの作成などができます。
@Service
public class UserService implements UserDetailsService {

    // UserRepositoryとPasswordEncoderを使うために、Autowiredで自動的に接続します。
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    // ユーザーネームをもとに、ユーザー情報を取得します。取得できない場合はエラーを返します。
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }
        return new UserPrincipal(user);
    }

    //ユーザーネームをもとに、ユーザー情報を探します。取得できない場合はnullを返します。
    public User findByUsername(String username) {
        return userRepository.findByUsername(username);
    }

    // 新しいユーザーを作成して保存します。パスワードは暗号化して保存します。
    @Transactional
    public void save(UserDto userDto) {
        // UserDtoからUserへの変換
        User user = new User();
        user.setUsername(userDto.getUsername());
        // パスワードを暗号化してから保存
        user.setPassword(passwordEncoder.encode(userDto.getPassword()));
        user.setEmail(userDto.getEmail());

        // データベースへの保存
        userRepository.save(user);
    }
}


サンプル画面

スクリーンショット 2023-07-25 10.56.37.png

スクリーンショット 2023-07-25 10.56.57.png

スクリーンショット 2023-07-25 10.57.27.png

スクリーンショット 2023-07-25 10.57.39.png

スクリーンショット 2023-07-25 11.12.51.png

スクリーンショット 2023-07-25 11.00.23.png

スクリーンショット 2023-07-25 11.11.13.png

GitHubに投稿したサンプル

あとがき

本記事ではchatGPT(GPT4)を利用して基本的なCRUDを使用した貸出管理システムを作成しました。データベースとJavaのフィールドの連携が上手くいかずエラーが発生したことで作業が中断されたことがしばしばありましたが、データベースのテーブルはSpringBootの自動構成に頼ることによってそれを回避できることを知れたのが今回の一番の収穫だったと思います。
データベースとJavaを連携させるためには必ずしも、テーブルのフィールド名とJavaのフィールド名を一致させれば良いというわけではなく、JavaによってSQL言語に変換される過程でフィールド名にアンダーバーが入ったりすることがあるので、次回以降その点には注意が必要と感じられました

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4