--- title: Spring Bootで作ったAPIのUIをClaudeに作らせるTips tags: ["Claude", "Spring Boot", "Java", "React", "Model Context Protocol"] categories: ["Programming", "Java", "org", "springframework", "boot"] date: 2025-05-15T07:10:26Z updated: 2025-05-15T08:58:09Z --- > TODO: Claude Code版を書く Spring Bootユーザーの多くはバックエンドエンジニアであり、フロントエンドの実装は苦手な方が多いのではないでしょうか。 私もそうなのですが、デモアプリ、サンプルアプリを作る際はAPIだけを実装し、curlで動作確認をしていることが多いです。 あるいは、簡素なUIを作ってお茶を濁すことが多いです。 一方でUIがあると同じAPIでも見栄えが良くなり、デモやサンプルアプリの印象がとても良くなります。 そこで、最近は専らLLM (Claude) を使ってSpring Bootで作ったAPIのUIを作成してもらっているので、どのようにしているのかを紹介します。 **目次** ### APIの設計ドキュメントを作成する まずはどのようなAPIを作成するのかを設計ドキュメントにまとめます。テキストベースであれば、フォーマットはどんな形式でも良いです。 ドキュメント時代をLLMに書かせても良いでしょう。 本記事ではサンプルアプリとして簡易掲示板を作成することにします。 次の簡単なAPIを設計しました。 https://gist.github.com/making/9f14e2024654d1134ad03d14fed00936 このドキュメントは英語で書かれていますが、日本語で書いてもClaudeは認識してくれます。 ### APIをSpring Bootで実装する では、このドキュメントに従ってAPIを実装していきます。この内容であれば、どのドキュメントをClaudeに読み込ませて、APIを実装させることも可能ですが、ここでは自分で実装することにします。 `/tmp`を作業ディレクトリにして、Spring Bootのプロジェクトを作成します。 ```bash cd /tmp ``` Spring BootのプロジェクトはSpring Initializrを使って作成します。 ```bash curl -s https://start.spring.io/starter.tgz \ -d artifactId=bbs-api \ -d baseDir=bbs-api \ -d type=maven-project \ -d dependencies=web,actuator,configuration-processor,native \ -d packageName=com.example.bbs \ -d applicationName=BbsApiApplication | tar -xzvf - cd bbs-api ``` まずは`Comment`クラスを作成します。 ```java cat < src/main/java/com/example/bbs/Comment.java package com.example.bbs; import java.time.Instant; import java.util.Objects; import java.util.UUID; /** * Record representing a comment on the bulletin board */ public record Comment(String id, String author, String content, Instant createdAt) { /** * Factory method to create a new comment from author and content */ public static Comment create(String author, String content) { return new Comment(UUID.randomUUID().toString(), Objects.requireNonNull(author, "'author' is required"), Objects.requireNonNull(content, "'content' is required"), Instant.now()); } } EOF ``` 次に、メインとなるComment APIの実装を`CommentController`に行います。ここではインメモリな実装にします。 ```java cat < src/main/java/com/example/bbs/CommentController.java package com.example.bbs; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * REST controller for bulletin board operations */ @RestController @RequestMapping("/api/v1") public class CommentController { // In-memory storage final ConcurrentMap comments = new ConcurrentHashMap<>(); /** * Create a new comment */ @PostMapping("/comments") public ResponseEntity createComment(@RequestBody CommentRequest request) { Comment comment = Comment.create(request.author(), request.content()); comments.put(comment.id(), comment); return ResponseEntity.status(HttpStatus.CREATED).body(comment); } /** * Get comments with pagination */ @GetMapping("/comments") public ResponseEntity getComments(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int limit, @RequestParam(defaultValue = "newest") String sort) { // Get all comments and sort them List allComments = new ArrayList<>(comments.values()); if ("newest".equalsIgnoreCase(sort)) { allComments.sort(Comparator.comparing(Comment::createdAt).reversed()); } else { allComments.sort(Comparator.comparing(Comment::createdAt)); } // Calculate pagination limit = Math.min(limit, 100); int totalComments = allComments.size(); int totalPages = (int) Math.ceil((double) totalComments / limit); // Apply pagination int startIndex = (page - 1) * limit; int endIndex = Math.min(startIndex + limit, totalComments); List pagedComments; if (startIndex < totalComments) { pagedComments = allComments.subList(startIndex, endIndex); } else { pagedComments = List.of(); } // Create pagination info PaginationInfo paginationInfo = new PaginationInfo(totalComments, totalPages, page, limit, page < totalPages, page > 1); // Create and return the response CommentsResponse response = new CommentsResponse(pagedComments, paginationInfo); return ResponseEntity.ok(response); } /** * Delete a comment by ID */ @DeleteMapping("/comments/{commentId}") public ResponseEntity deleteComment(@PathVariable String commentId) { comments.remove(commentId); return ResponseEntity.noContent().build(); } /** * Request DTO for comment creation */ public record CommentRequest(String author, String content) { } /** * Response DTO for paginated comments */ public record CommentsResponse(List data, PaginationInfo pagination) { } /** * DTO for pagination metadata */ public record PaginationInfo(int totalComments, int totalPages, int currentPage, int limit, boolean hasNextPage, boolean hasPrevPage) { } } EOF ``` Integration Testも作成します。 ```java cat < src/test/java/com/example/bbs/BbsApiIntegrationTest.java package com.example.bbs; import com.example.bbs.CommentController.CommentRequest; import com.example.bbs.CommentController.CommentsResponse; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; /** * Integration test for Bulletin Board REST API */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class BbsApiIntegrationTest { @LocalServerPort int port; @Autowired TestRestTemplate restTemplate; @Autowired CommentController commentController; String baseUrl; @BeforeEach public void setUp() { baseUrl = "http://localhost:" + port + "/api/v1"; commentController.comments.clear(); } /** * Test creating a comment */ @Test public void testCreateComment() { // Create a comment request CommentRequest request = new CommentRequest("TestUser", "This is a test comment"); // Send POST request ResponseEntity response = restTemplate.postForEntity(baseUrl + "/comments", request, Comment.class); // Verify response assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().author()).isEqualTo("TestUser"); assertThat(response.getBody().content()).isEqualTo("This is a test comment"); assertThat(response.getBody().id()).isNotNull(); assertThat(response.getBody().createdAt()).isNotNull(); } /** * Test creating multiple comments and fetching with pagination */ @Test public void testCreateAndGetComments() { List createdComments = new ArrayList<>(); // Create multiple comments for (int i = 1; i <= 5; i++) { CommentRequest request = new CommentRequest("User" + i, "Test comment " + i); ResponseEntity response = restTemplate.postForEntity(baseUrl + "/comments", request, Comment.class); createdComments.add(response.getBody()); } // Test getting comments with default parameters ResponseEntity response = restTemplate.exchange(baseUrl + "/comments", HttpMethod.GET, null, new ParameterizedTypeReference<>() { }); // Verify response assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().data()).isNotNull(); assertThat(response.getBody().pagination()).isNotNull(); // Verify pagination assertThat(response.getBody().pagination().totalComments()).isEqualTo(createdComments.size()); assertThat(response.getBody().data()).isNotEmpty(); // Test that comments are sorted by newest first (default) List comments = response.getBody().data(); for (int i = 0; i < comments.size() - 1; i++) { assertThat(comments.get(i).createdAt()).isAfterOrEqualTo(comments.get(i + 1).createdAt()); } } /** * Test pagination with different parameters */ @Test public void testPagination() { List createdComments = new ArrayList<>(); // Create multiple comments for (int i = 1; i <= 5; i++) { CommentRequest request = new CommentRequest("User" + i, "Test comment " + i); ResponseEntity response = restTemplate.postForEntity(baseUrl + "/comments", request, Comment.class); createdComments.add(response.getBody()); } // Test with limit = 2 ResponseEntity response = restTemplate.exchange(baseUrl + "/comments?limit=2", HttpMethod.GET, null, new ParameterizedTypeReference<>() { }); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().data()).hasSize(2); assertThat(response.getBody().pagination().limit()).isEqualTo(2); // Calculate expected total pages int expectedTotalPages = (int) Math.ceil((double) createdComments.size() / 2); assertThat(response.getBody().pagination().totalPages()).isEqualTo(expectedTotalPages); // Test second page response = restTemplate.exchange(baseUrl + "/comments?limit=2&page=2", HttpMethod.GET, null, new ParameterizedTypeReference() { }); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().data()).isNotEmpty(); assertThat(response.getBody().pagination().currentPage()).isEqualTo(2); // Test oldest sort order response = restTemplate.exchange(baseUrl + "/comments?sort=oldest", HttpMethod.GET, null, new ParameterizedTypeReference() { }); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); List comments = response.getBody().data(); for (int i = 0; i < comments.size() - 1; i++) { assertThat(comments.get(i).createdAt()).isBeforeOrEqualTo(comments.get(i + 1).createdAt()); } } /** * Test deleting a comment */ @Test public void testCreateAndDeleteComment() { // Create a comment request CommentRequest request = new CommentRequest("TestUser", "This is a test comment"); // Send POST request ResponseEntity created = restTemplate.postForEntity(baseUrl + "/comments", request, Comment.class); // Delete the first comment Comment commentToDelete = created.getBody(); restTemplate.delete(baseUrl + "/comments/" + commentToDelete.id()); // Get all comments to verify deletion ResponseEntity response = restTemplate.exchange(baseUrl + "/comments", HttpMethod.GET, null, new ParameterizedTypeReference<>() { }); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().pagination().totalComments()).isEqualTo(0); } /** * Test complete API flow - create, get and delete */ @Test public void testCompleteApiFlow() { // Create a new comment CommentRequest request = new CommentRequest("FlowTestUser", "Testing complete API flow"); ResponseEntity createResponse = restTemplate.postForEntity(baseUrl + "/comments", request, Comment.class); assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); Comment createdComment = createResponse.getBody(); assertThat(createdComment).isNotNull(); // Get comments and ensure the new one is included ResponseEntity getResponse = restTemplate.exchange(baseUrl + "/comments", HttpMethod.GET, null, new ParameterizedTypeReference<>() { }); assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK); boolean commentFound = getResponse.getBody() .data() .stream() .anyMatch(comment -> comment.id().equals(createdComment.id())); assertThat(commentFound).isTrue(); // Delete the comment restTemplate.delete(baseUrl + "/comments/" + createdComment.id()); // Verify comment was deleted getResponse = restTemplate.exchange(baseUrl + "/comments", HttpMethod.GET, null, new ParameterizedTypeReference<>() { }); commentFound = getResponse.getBody() .data() .stream() .anyMatch(comment -> comment.id().equals(createdComment.id())); assertThat(commentFound).isFalse(); } } EOF ``` 必須ではないですが、エラーのJSONにエラーメッセージを出力するためのプロパティを設定します。 ```properties cat <> src/main/resources/application.properties server.error.include-message=always EOF ``` テスト&ビルドを実行し、作成されたjarファイルを実行します。 ```bash ./mvnw clean package java -jar target/bbs-api-0.0.1-SNAPSHOT.jar ``` curlでAPIを叩いてみます。 ```bash $ curl localhost:8080/api/v1/comments {"data":[],"pagination":{"totalComments":0,"totalPages":0,"currentPage":1,"limit":20,"hasNextPage":false,"hasPrevPage":false}} ``` ### UIをClaudeに作成してもらう 次に、作成したAPIに対するUIを作成してもらいます。 本記事ではClaude Desktopを使用します。また、[Claude CodeをMCPサーバーとして使用し](https://docs.anthropic.com/en/docs/claude-code/tutorials#use-claude-code-as-an-mcp-server)、 Claude Desktopからローカルのファイルを直接編集してもらいます。 > [!NOTE] > Claude CodeをMCPサーバーとして使用してClaude Desktopから使うための設定方法は次の記事が参考になります。
> https://zenn.dev/myui/articles/7d9c8ba9231b49 最初に作成した設計ドキュメントをClaude Desktopに添付し、次のプロンプトを与えます。 ``` 添付のAPI設計書に従って、簡易BBSを作成します。APIはSpring Boot、UIはReact + React Router + Typescript + vite + Tailwindで実装します。 APIは実装済みで http://localhost:8080 でアクセス可能です。最終的には同じサーバーに同梱されるので、UIからはviteのproxyを経由して`/api/v1`でアクセスできるようにしてください。 あなたはtoolを使ってモダンなUIを実装してください。アーティファクトは使わないでください。 /tmp/bbs-api/uiで作業してください。コード中のコメントは英語にしてください。 HTTPアクセスはfetch + swrを使用してください。 Tailwindでライトモードとダークモードも実装してください。また、Tailwindのクラスを場当たり的に使うのではなく、メンテナンス性が高くなるようにできるだけコンポーネント化を意識してください。 また最新のパッケージだと互換性の問題が発生する可能性があるので、パッケージをインストールする際はあなたが知っているバージョンを必ず明示してください。 ``` ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/c244e4d6-dca0-4cb9-84d9-4c6b70e1dc57.png) Claude DesktopがClaude Codeを使って、`/tmp/bbs-api/ui`に対して作業を開始します。 > [!NOTE] > このチャットの実際のやり取りは次のリンクから参照できます。
> https://claude.ai/share/a00afde5-0e3e-47bc-a7dc-97541ae5c1ac 次のようなレスポンスが返り、作業が完了したら、動作確認します。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/5736db65-9196-49c1-83ba-1519d2db690d.png) ``` cd /tmp/bbs-api/ui npm run dev ``` ブラウザで`http://localhost:5173`にアクセスすると、次のような画面が表示されます。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/fe91fceb-87ac-44a0-9950-bdbef4afce4d.png) ダークモードもサポートされています。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/2cbf0f19-f85d-4515-97d2-3b0d32ac4d5d.png) どんなUIが実装されるかはガチャですが、デザインが苦手な自分で実装するよりも圧倒的に早く、かつ見栄えの良いUIが実装されます。 少し色合いを変えてみます。チャットに続けて、次のプロンプトを与えます。 ``` 黄色を基調としたカラーにしてください ``` 今度は次のようなUIが実装されました。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/c6ebf0ac-f0c7-4ebc-9213-cb0fb1cd7245.png) ダークモードは次のようになります。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/eb090529-985e-493f-ad62-2ccfe0520f14.png) ちなみに、コメントを増やすとパジネーションリンクも表示されます。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/b47e34b5-cd1c-49b3-9eec-6d5dd0e6c3b7.png) さて、ここまでできたら、Claudeが作成したソースコードを一旦Gitコミットします。 この後引き続きタスクを与えて、思うように実装されなかった場合に戻せるように、タスク単位でGitコミットしておくと良いでしょう。 ``` cd /tmp/bbs-api git init git add ui git commit -m "Add initial UI" ``` コンテキストが大きくなりすぎないように、追加のタスクは別チャットで改めて依頼した方が良いです。 追加のタスクは次のようなプロンプトを与えると、継続して作業をしてくれます。 ``` /tmp/bbs-api/uiはReact + React Router + Typescript + vite + Tailwindで実装された管理BBSです。 このUIに対してあなたにtoolで改修してもらいたいです。以下のタスクをお願いします。 * <ここにタスクを記述> 最新のパッケージだと互換性の問題が発生する可能性があるので、パッケージをインストールする際はあなたが知っているバージョンを必ず明示してください。 ``` UIのソースコードは次のように`npm run lint`と`npm run build`がパスするまでClaudeに修正してもらいます。 ``` $ npm run lint > ui@0.0.0 lint > eslint src --ext ts,tsx --max-warnings 0 ``` ``` $ npm run build > ui@0.0.0 build > vite build vite v4.3.9 building for production... ✓ 375 modules transformed. dist/index.html 0.46 kB │ gzip: 0.30 kB dist/assets/index-9eb533d8.css 15.13 kB │ gzip: 3.01 kB dist/assets/index-7fc96fff.js 185.93 kB │ gzip: 59.67 kB │ map: 575.06 kB ✓ built in 652ms ``` lintエラーやbuildのエラーは頻繁に出ます。これらのエラーが何度チャットしてもfixされないこともあるので、コンテキストが増大しないように、lintやbuildエラーは別タスクで行った方が良いです。 次のようなプロンプトを与えて、エラーをfixしてください。 ``` /tmp/bbs-api/uiはReact + React Router + Typescript + vite + Tailwindで実装された管理BBSです。 このUIに対してあなたにtoolで改修してもらいたいです。以下のタスクをお願いします。 * `npm run lint`を実行すると次のエラーが発生するので解消してください xxxxxxxxxxxxxxxx 最新のパッケージだと互換性の問題が発生する可能性があるので、パッケージをインストールする際はあなたが知っているバージョンを必ず明示してください。 ``` UIの作業が完了したら、一旦ソースコードをコミットします。 ``` cd /tmp/bbs-api git add . git commit -m "Add REST API" ``` ### APIとUIを統合する 最後にAPIとUIを統合します。 まずはUIの`package.json`を次のように修正し、`npm run build`でビルドしたアセットをSpring Bootの`target/classes/META-INF/resources`に出力するようにします。 ```json { // ... "scripts": { // ... "build": "tsc && vite build --outDir ../target/classes/META-INF/resources --emptyOutDir", // ... }, // ... } ``` > [!TIP] > クラスパス直下の`/META-INF/resources`はSpring Bootのデフォルトの静的リソースの配置場所です。 まず、APIのpom.xmlにfrontend-maven-pluginを追加し、Mavenによるコンパイル時に、`ui`ディレクトリに移動して、`npm install`と`npm run build`を実行するようにします。 ```xml com.github.eirslett frontend-maven-plugin 1.15.1 target v22.15.0 install node and npm install-node-and-npm npm install ui npm install ${project.basedir}/ui npm run lint ui npm test run lint ${project.basedir}/ui npm run build ui npm compile run build ${project.basedir}/ui ``` Tipですが、Paketo Buildpackでコンテナ化するとファイルの作成時刻が固定になります。 Spring Bootはデフォルトでファイルの作成時刻が`Last-Modified`ヘッダーに設定されます。 その結果、静的リソースは過去からずっと変更されていないものとしてキャッシュされてしまいます。 それを防ぐために`application.properties`に`spring.web.resources.cache.use-last-modified=false`を設定します。 ファイルの作成時刻を`Last-Modified`ヘッダーに設定する代わりに、キャッシュの有効期限を`spring.web.resources.cache.period`で指定します。 次のようにプロパティをapplication.propertiesに追加します。 ```properties cat <> src/main/resources/application.properties server.compression.enabled=true spring.web.resources.cache.period=365d spring.web.resources.cache.use-last-modified=false EOF ``` ここまで設定できたら、ビルドします。 ``` ./mvnw clean package ``` `target/classes/META-INF/resources/`以下に`ui`のビルドアセットが出力されていることを確認します。 ``` $ ls -la target/classes/META-INF/resources/ total 16 drwxr-xr-x@ 5 toshiaki wheel 160 5 15 14:14 . drwxr-xr-x@ 3 toshiaki wheel 96 5 15 14:14 .. drwxr-xr-x@ 5 toshiaki wheel 160 5 15 14:14 assets -rw-r--r--@ 1 toshiaki wheel 457 5 15 14:14 index.html -rw-r--r--@ 1 toshiaki staff 1497 5 15 14:14 vite.svg ``` 作成されたjarファイルを実行します。 ``` java -jar target/bbs-api-0.0.1-SNAPSHOT.jar ``` `http://localhost:8080`にアクセスし、Claudeが作成したUIが表示されると成功です。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/6783da50-e829-425e-8fdb-6bfdb6986811.png) > [!TIP] > > 今回の題材は簡単すぎてReact Routerによるフロントエンドのルーティングが発生しませんでした。
> React Routerでルーティングを行う場合は、フロントエンドでルーティングされるパスにサーバーサイドで直接アクセスした場合にindex.htmlへフォワードされるように
> 次のようなControllerを書く必要があります。 > > ```java > import org.springframework.stereotype.Controller; > import org.springframework.web.bind.annotation.GetMapping; > > @Controller > public class HomeController { > > @GetMapping(path = { "/", "/comments", "/comments/**" }) > public String index() { > return "forward:/index.html"; > } > > } > ``` > ここまでの内容をGitにコミットします。 ``` git add -A git commit -m "Integrate UI and API" ``` 実際に作成されたソースコードはこちら https://github.com/making/bbs-api です。 もし、スタイルが気に入らない場合は、次のようなプロンプトを与えて、スタイルを変更してもらいます。 例えば次のように依頼できます。 ``` /tmp/bbs-api/uiはReact + React Router + Typescript + vite + Tailwindで実装された管理BBSです。 このUIに対してあなたにtoolで改修してもらいたいです。以下のタスクをお願いします。 * Minecraft風な見栄えにしてください 最新のパッケージだと互換性の問題が発生する可能性があるので、パッケージをインストールする際はあなたが知っているバージョンを必ず明示してください。 ``` 次のような仕上がりになります。 ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/d2acba52-8cec-4d9d-99ca-8e963f1ff8e5.png) ![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/07673912-8f7a-46bb-ba14-afab68e34f9b.png) Minecraft版のソースコードはこちらです https://github.com/making/bbs-api/tree/minecraft-style --- デモアプリや小規模アプリなアプリであれば上記の手法でClaudeにUIを作成してもらえ、圧倒的に生産性が上がります。 このブログを含めて、以下のアプリは今回の記事とほぼ同じ手法でUIをClaudeに作成してもらいました。 * https://github.com/making/spring-batch-dashboard * https://github.com/categolj/room-reservation * https://github.com/making/hello-spring-ai * https://github.com/making/oauth2-sso-demo * https://github.com/making/ssr-react-spring-boot-graalvm-js * https://github.com/categolj/blog-frontend ソースコードが大きくなるとコンテキストサイズの制限が出てきそうです。 また、どこまでメンテナンス可能なソースコードを生成してくれるかは未検証です。