<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>エンジニアライブログ</title>
	<atom:link href="https://tomoyuki65.com/feed" rel="self" type="application/rss+xml" />
	<link>https://tomoyuki65.com</link>
	<description></description>
	<lastBuildDate>Thu, 08 Oct 2026 21:28:05 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
<atom:link rel="hub" href="https://pubsubhubbub.appspot.com"/>
<atom:link rel="hub" href="https://pubsubhubbub.superfeedr.com"/>
<atom:link rel="hub" href="https://websubhub.com/hub"/>
<atom:link rel="self" href="https://tomoyuki65.com/feed"/>
	<item>
		<title>Hono入門｜TypeScriptでAPIを作成する方法【Node.js・Cloudflare対応】</title>
		<link>https://tomoyuki65.com/hono-typescript-api</link>
					<comments>https://tomoyuki65.com/hono-typescript-api#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Sun, 04 Oct 2026 15:58:02 +0000</pubDate>
				<category><![CDATA[4. プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20621</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 2026年現在、TypeScriptでAPIを開発するフレームワークとしては「Hono」が注目されています。 特に個人開発や小規模開発の本番環境...</p>
The post <a href="https://tomoyuki65.com/hono-typescript-api">Hono入門｜TypeScriptでAPIを作成する方法【Node.js・Cloudflare対応】</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img fetchpriority="high" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-0.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-20739" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-0.png 672w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-0-300x214.png 300w" sizes="(max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>2026年現在、TypeScriptでAPIを開発するフレームワークとしては<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「Hono」</strong></span>が注目されています。</p>
<p><span>特に個人開発や小規模開発の本番環境として</span>Cloudflare Workersを<span>利用するとコストを抑えやすく、<strong><span style="color: #3366ff;">Honoはそのような環境を含め、さまざまな環境にデプロイできる</span></strong>ため、有効な選択肢の一つになっています。</span></p>
<p>この記事では、そんなHono（TypeScript）でAPIを作成する方法をご紹介します。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Hono入門｜TypeScriptでAPIを作成する方法【Node.js・Cloudflare対応】</h2>
<h3>Honoとは？</h3>
<p>Hono（ホノ）は、TypeScript / JavaScript向けの軽量かつ高速なWebアプリケーションフレームワークです。</p>
<p>Web標準APIをベースとしており、マルチランタイムに対応しているため、<span style="color: #3366ff;"><strong>Node.jsだけでなく、Deno・Bun・Cloudflare Workersなど、様々な環境で動作する</strong></span>のが特徴です。</p>
<p>&nbsp;</p>
<h3>Honoを使うメリット</h3>
<p>Honoを使うメリットとしては、軽量かつシンプルなAPIを、様々な実行環境で構築できる点になります。</p>
<p>また、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>フロントエンド開発でTypeScriptを使用している場合、バックエンドにもTypeScriptを採用することでプログラミング言語を統一</strong></span>でき、<span style="color: #3366ff;"><strong>エンジニア採用の面でも採用母数を広げられる可能性</strong></span>があります。</p>
<p>&nbsp;</p>
<h3>HonoでAPIを作る前の準備</h3>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>今回はまずnode環境でローカル開発環境を構築</strong></span>するため、<strong><span style="color: #ff0000;">事前にnodeを使えるようにする必要</span></strong>があります。</p>
<p>私はnodeのバージョン管理ツールとしては<strong><span style="border-bottom: 2px solid #be3144;">「volta」</span></strong>を気に入っているため、今回はこれをご紹介しますが、インストールしたい場合は<span>curlコマンドを利用し、以下のコマンドを実行すると可能です。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code><span>$ </span><span class="token function">curl</span><span> https://get.volta.sh </span><span class="token operator">|</span><span> </span><span class="token function">bash</span></code></pre>
</div>
<p>&nbsp;</p>
<p><span>インストール後、以下のコマンドでパスが通っているか確認して下さい。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ volta -v</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、nodeをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ volta install node@24.21.0</code></pre>
</div>
<p><strong><span style="color: #ff0000;">※ここでは2026年9月時点でLTS版のバージョン「24.21.0」を使います。尚、チーム開発などではnodeのバージョンは固定推薦です。</span></strong></p>
<p>&nbsp;</p>
<p>インストール後、以下のコマンドでパスが通っているか確認して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ node -v</code></pre>
</div>
<p>&nbsp;</p>
<p>次にnodeのパッケージマネージャーとしては、デフォルトのnpmではなく、最近人気の<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「pnpm」</strong></span>を利用します。</p>
<p>MacOSでよく使われるパッケージ管理ツールの<span>Homebrew経由でインストールするなら、以下のコマンドを実行すると可能です。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ brew install pnpm</code></pre>
</div>
<p>&nbsp;</p>
<p>インストール後、以下のコマンドでパスが通っているか確認して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm -v</code></pre>
</div>
<p>&nbsp;</p>
<h3>Honoの新規プロジェクトを作成</h3>
<p>次に以下のコマンドを実行し、Honoの新規プロジェクトを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code><span>$ </span><span class="token function">mkdir</span><span> hono-sample </span><span class="token operator">&amp;&amp;</span><span> </span><span class="token function"><span class="hljs-built_in">cd</span></span><span> hono-sample
$ pnpm create hono@latest .</span></code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、テンプレートを選択できるので、今回は「nodejs」を選びます。</p>
<img decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-1.png" alt="" width="922" height="332" class="aligncenter size-full wp-image-20631" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-1.png 922w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-1-300x108.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-1-768x277.png 768w" sizes="(max-width: 922px) 100vw, 922px" />
<p>&nbsp;</p>
<p>次にプロジェクトの依存関係をインストールするか聞かれるので、「y」を入力します。</p>
<img decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-2.png" alt="" width="846" height="234" class="aligncenter size-full wp-image-20632" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-2.png 846w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-2-300x83.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-2-768x212.png 768w" sizes="(max-width: 846px) 100vw, 846px" />
<p>&nbsp;</p>
<p>次にパッケージマネージャーにどれを使うか聞かれるので、「pnpm」を選択します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-3.png" alt="" width="874" height="300" class="aligncenter size-full wp-image-20633" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-3.png 874w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-3-300x103.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-3-768x264.png 768w" sizes="auto, (max-width: 874px) 100vw, 874px" />
<p>&nbsp;</p>
<p>実行後、各種ファイルが作成され、以下のように完了メッセージが出力されます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-4.png" alt="" width="822" height="288" class="aligncenter size-full wp-image-20634" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-4.png 822w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-4-300x105.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-4-768x269.png 768w" sizes="auto, (max-width: 822px) 100vw, 822px" />
<p>&nbsp;</p>
<p>次にテキストエディタでプロジェクトを確認し、以下のように各種ファイルが作成されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-5.png" alt="" width="1038" height="574" class="aligncenter size-full wp-image-20635" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-5.png 1038w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-5-300x166.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-5-1024x566.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-5-768x425.png 768w" sizes="auto, (max-width: 1038px) 100vw, 1038px" />
<p>&nbsp;</p>
<h3>nodeとpnpmのバージョン固定</h3>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>nodeとpnpmのバージョン固定</strong></span>を行います。まずは以下のコマンドを実行し、ファイル「.npmrc」を作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch .npmrc</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイル「.npmrc」は以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".npmrc"><code>engine-strict=true</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「package.json」に対して以下のように<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「engines」、「packageManager」を追加</strong></span>します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="package.json"><code>{

・・・

  "engines": {
    "node": "24.21.0",
    "pnpm": "12.6.0"
  },
  "packageManager": "pnpm@12.6.0",

・・・</code></pre>
</div>
<p><span style="color: #ff0000;">※pnpmのバージョンはインストール時に「12.6.0」だったのでこれで固定しています。必要に応じて修正して下さい。</span></p>
<p>&nbsp;</p>
<h3>ローカルサーバーを起動して試す</h3>
<p>次に以下のコマンドを実行し、ローカルサーバーの起動を試します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm run dev</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のようにサーバーが起動します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-6.png" alt="" width="761" height="125" class="aligncenter wp-image-20636" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-6.png 706w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-6-300x49.png 300w" sizes="auto, (max-width: 761px) 100vw, 761px" />
<p><span style="color: #ff0000;">※サーバーを止めたい場合は、ターミナルをアクティブ状態にしてショートカットキー「control + c」を実行して下さい。</span></p>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3000」を開き、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-7.png" alt="" width="948" height="470" class="aligncenter size-full wp-image-20637" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-7.png 948w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-7-300x149.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-7-768x381.png 768w" sizes="auto, (max-width: 948px) 100vw, 948px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>サンプルAPIを作成する（DDDを考慮したディレクトリ構成）</h2>
<p>次にサンプルAPIを作成してみますが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>DDD（ドメイン駆動設計）を考慮</strong></span>して、以下のようなディレクトリ構成で各種ファイルを作るようにしていきます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>/hono-sample
 └─ /src
     |
     ├─ /core（中核の業務領域）
     |   |
     |   └─ /[domainName]（ドメインモジュール）
     |       |
     |       ├─ /domain（ドメイン）
     |       |   |
     |       |   ├─ [entityName].ts（エンティティ）
     |       |   |
     |       |   ├─ [valueObjectName].ts（値オブジェクト）
     |       |   |
     |       |   ├─ [domainServiceName]Service.ts（ドメインサービス）
     |       |   |
     |       |   ├─ [repositoryName]Repository.ts（リポジトリのインターフェース）
     |       |   |
     |       |   └─ [gatewayName]Gateway.ts（外部サービス用のインターフェース）
     |       |
     |       |
     |       ├─ /usecase（ユースケース層）
     |       |
     |       └─ /infrastructure（ドメイン用のインフラストラクチャ層）
     |           |
     |           ├─ /persistence（パーシステンス層）
     |           |   |
     |           |   └─ /pg（PostgreSQL用）
     |           |       |
     |           |       ├─ /command（書き込み）
     |           |       |
     |           |       └─ /query（読み込み）
     |           |
     |           └─ /external（外部サービスの実装）
     |
     ├─ /supporting（補完的なの業務領域）
     |   |
     |   └── /[supportingName]（サービス層）
     |        |
     |        └─ useCase.ts（ユースケース）
     |
     ├─ /generic（一般的な業務領域）
     |
     ├─ /shared（横断関心）
     |   |
     |   └─ /logger（共通ロガーの定義）
     |       |
     |       └─ logger.ts
     |
     ├─ /di（DIコンテナ層）
     |   |
     |   └─ nodeContainer.ts（Node.js環境用）
     |
     ├─ /infrastructure（共通のインフラストラクチャ層）
     |   |
     |   ├─ /logger
     |   |   |
     |   |   └─ consoleLogger.ts（consoleロガーの実装）
     |   |
     |   └─ /database
     |       |
     |       └─ /drizzle（Drizzle ORM用）
     |           |
     |           └─ /pg（PostgreSQL用）
     |               |
     |               ├─ /migration（マイグレーション関連）
     |               |
     |               ├─ /schema（スキーマ定義）
     |               |
     |               └─ <span>client.ts（接続設定）</span>
     |
     ├─ /presentation（プレゼンテーション層）
     |   |
     |   └─ /http
     |       |
     |       ├─ /middleware（ミドルウェア）
     |       |
     |       ├─ /core
     |       |   |
     |       |   └─ /[domainName]
     |       |       |
     |       |       ├─ handler.ts（ハンドラー部分）
     |       |       |
     |       |       └─ routes.ts（ルーティング部分）
     |       |
     |       ├─ /supporting
     |       |   |
     |       |   └─ /[supportingName]
     |       |       |
     |       |       ├─ handler.ts（ハンドラー部分）
     |       |       |
     |       |       └─ routes.ts（ルーティング部分）
     |       |
     |       ├─ /generic
     |       |
     |       └─ routesNode.ts（Node.js環境用のルーティング設定）
     |
     ├─ /runtime（ランタイム層）
     |   |
     |   └─ /node（Node.js環境用）
     |       |
     |       ├─ config.ts（コンフィグ設定）
     |       |
     |       ├─ app.ts（アプリ設定）
     |       |
     |       └─ server.ts（サーバー設定）
     |
     ├─ /tests（結合テスト用）
     |
     └─ app.ts（アプリ全体の定義）</code></pre>
</div>
<p><strong><span style="color: #ff0000;">※ディレクトリ構成はDDD（ドメイン駆動設計）を前提としますが、アプリ全体に適用するのは本質的でないため、３つの業務領域（中核、補完的、一般的）に分類することを前提としています。また、Honoは異なるランタイム環境で実行できるメリットがあるため、アプリ全体の定義「app.ts」はランタイムの設定「runtime/node」に依存しないようにしています。</span></strong></p>
<p>&nbsp;</p>
<div class="supplement boader"><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/domain-driven-design-essentials" target="_blank" rel="noopener">・ドメイン駆動設計（Domain-Driven Design / DDD）の本質と大事なこと</a></p>
</div>
<p>&nbsp;</p>
<h3>pnpmライブラリのインストール</h3>
<p>まずは以下のコマンドを実行し、各種ライブラリを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add -D vitest @biomejs/biome
$ pnpm add zod @hono/zod-validator</code></pre>
</div>
<p><span style="color: #ff0000;">※テスト用に「vitest」、フォーマッターとリンター用に「@biomejs/biome」、バリデーションチェック用に「zod」、「@hono/zod-validator」を使います。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、biomeの初期化をします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm biome init</code></pre>
</div>
<p>&nbsp;</p>
<h3>共通ロガーの作成</h3>
<p>次に以下のコマンドを実行し、各種ファイルを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/shared/logger &amp;&amp; touch src/shared/logger/logger.ts
$ mkdir -p src/infrastructure/logger &amp;&amp; touch src/infrastructure/logger/consoleLogger.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/shared/logger/logger.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/shared/logger/logger.ts"><code>// ログ用コンテキストの型定義
export type LogContext = {
  requestId?: string;
  [key: string]: unknown;
};

// ロガーのインターフェース定義
export interface Logger {
  info(message: string, context?: LogContext): void;
  warn(message: string, context?: LogContext): void;
  error(message: string, context?: LogContext): void;
  debug(message: string, context?: LogContext): void;
  withRequestId(requestId: string): Logger;
};</code></pre>
</div>
<p><span style="color: #ff0000;">※共通ロガーのインターフェース定義のみ行う。</span></p>
<p>&nbsp;</p>
<p>・「src/infrastructure/logger/consoleLogger.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/infrastructure/logger/consoleLogger.ts"><code>import type { LogContext, Logger } from "../../shared/logger/logger.js";

// ロガーの作成用関数
const createLogger = (baseContext: LogContext = {}): Logger =&gt; {
  // ログ出力用の関数定義
  const log = (level: string, message: string, context: LogContext = {}) =&gt; {
    console.log(
      JSON.stringify({
        level,
        message,
        ...baseContext,
        ...context,
        timestamp: new Date().toISOString(),
      }),
    );
  };

  return {
    info(message, context) {
      log("INFO", message, context);
    },

    warn(message, context) {
      log("WARN", message, context);
    },

    error(message, context) {
      log("ERROR", message, context);
    },

    debug(message, context) {
      log("DEBUG", message, context);
    },

    withRequestId(requestId) {
      return createLogger({
        ...baseContext,
        requestId,
      });
    },
  };
};

// ロガーのインスタンス
export const logger = createLogger();</code></pre>
</div>
<p><span style="color: #ff0000;">※共通ロガーの実装はインフラストラクチャ層で行う。</span></p>
<p>&nbsp;</p>
<h3>GETとPOSTメソッドを利用するサンプルAPIを作成</h3>
<p>次にGETとPOSTメソッドを利用するようなサンプルAPIを作成するため、以下のコマンドを実行して各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/supporting/sample &amp;&amp; touch src/supporting/sample/useCase.ts src/supporting/sample/useCase.test.ts
$ mkdir -p src/tests/integration/supporting/sample &amp;&amp; touch src/tests/integration/supporting/sample/sample.test.ts
$ mkdir -p src/presentation/http/supporting/sample &amp;&amp; touch src/presentation/http/supporting/sample/handler.ts
$ touch src/presentation/http/supporting/sample/routes.ts src/presentation/http/supporting/sample/routes.test.ts
$ mkdir -p src/presentation/http/middleware &amp;&amp; touch src/presentation/http/middleware/requestLogger.ts
$ touch src/app.ts .env
$ rm src/index.ts</code></pre>
</div>
<p><span style="color: #ff0000;">※既存の「src/index.ts」は不要になるので削除する。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/supporting/sample/useCase.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/supporting/sample/useCase.ts"><code>import type { Logger } from "../../shared/logger/logger.js";

type SampleTextInput = {
  text: string;
};

export class SampleHelloUseCase {
  execute(logger: Logger) {
    // ログ出力
    logger.info("SampleHelloUseCaseを実行！");

    return {
      message: "Hello World !!",
    };
  }
}

export class SampleTextUseCase {
  execute(logger: Logger, input: SampleTextInput) {
    // ログ出力
    logger.info("SampleTextUseCaseを実行！", { text: input.text });

    return {
      message: input.text,
    };
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※ユースケースはクラスで定義する。</span></p>
<p>&nbsp;</p>
<p>・「src/supporting/sample/useCase.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/supporting/sample/useCase.test.ts"><code>import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Logger } from "../../shared/logger/logger.js";
import { SampleHelloUseCase, SampleTextUseCase } from "./useCase.js";

// ロガーのモック化
const createMockLogger = (): Logger =&gt; ({
  info: vi.fn(),
  warn: vi.fn(),
  error: vi.fn(),
  debug: vi.fn(),
  withRequestId: vi.fn(),
});

let logger: Logger;

// テスト前の共通セットアップ
beforeEach(() =&gt; {
  logger = createMockLogger();
});

describe("SampleHelloUseCase", () =&gt; {
  it("Hello Worldを返す", () =&gt; {
    // ユースケース作成
    const useCase = new SampleHelloUseCase();

    // ユースケース実行
    const result = useCase.execute(logger);

    // 検証
    expect(result).toEqual({
      message: "Hello World !!",
    });

    expect(logger.info).toHaveBeenCalledWith("SampleHelloUseCaseを実行！");
  });
});

describe("SampleTextUseCase", () =&gt; {
  it("渡されたtextを返す", () =&gt; {
    // ユースケース作成
    const useCase = new SampleTextUseCase();

    // ユースケース実行
    const result = useCase.execute(logger, {
      text: "テストメッセージ",
    });

    // 検証
    expect(result).toEqual({
      message: "テストメッセージ",
    });

    expect(logger.info).toHaveBeenCalledWith("SampleTextUseCaseを実行！", {
      text: "テストメッセージ",
    });
  });
});</code></pre>
</div>
<p><span style="color: #ff0000;">※ユースケースのユニットテスト</span></p>
<p>&nbsp;</p>
<p>・「src/tests/integration/supporting/sample/sample.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/tests/integration/supporting/sample/sample.test.ts"><code>import { testClient } from "hono/testing";
import { describe, expect, it, vi } from "vitest";
import { createNodeApp } from "../../../../runtime/node/app.js";
import type { NodeConfig } from "../../../../runtime/node/config.js";
import { config } from "../../../../runtime/node/config.js";

// ログ出力の無効化
vi.spyOn(console, "log").mockImplementation(() =&gt; {});

// テスト用コンフィグ変数
let testConfig: NodeConfig;

// テスト用クライアント作成関数
const createTestClient = () =&gt; {
  testConfig = {
    ...config,
  };

  return testClient(createNodeApp(testConfig));
};

describe("GET /api/v1/sample", () =&gt; {
  const client = createTestClient();

  it("メッセージに「Hello World !!」を出力し、正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.sample.$get();

    // 検証
    expect(response.status).toBe(200);

    // レスポンス結果の検証
    const body = await response.json();
    expect(body).toEqual({
      message: "Hello World !!",
    });
  });
});

describe("POST /api/v1/sample", () =&gt; {
  const client = createTestClient();

  it("メッセージに「サンプルテキスト！」を出力し、正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.sample.$post({
      json: {
        text: "サンプルテキスト！",
      },
    });

    // 検証
    expect(response.status).toBe(200);

    // レスポンス結果の検証
    const body = await response.json();
    expect(body).toEqual({
      message: "サンプルテキスト！",
    });
  });
});</code></pre>
</div>
<p><span style="color: #ff0000;">※インテグレーションテスト。Honoの型推論が効いていれば「testClient」を使ってテスト可能。</span></p>
<p>&nbsp;</p>
<p>・「src/presentation/http/supporting/sample/handler.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/supporting/sample/handler.ts"><code>import type { Context } from "hono";
import type {
  SampleHelloUseCase,
  SampleTextInput,
  SampleTextUseCase,
} from "../../../../supporting/sample/useCase.js";

export const createSampleHelloHandler =
  (useCase: SampleHelloUseCase) =&gt; (c: Context) =&gt; {
    // 共通ロガー取得
    const logger = c.get("logger");

    // ユースケース実行
    const result = useCase.execute(logger);

    return c.json(result);
};

export const createSampleTextHandler =
  (useCase: SampleTextUseCase) =&gt; (c: Context, input: SampleTextInput) =&gt; {
    // 共通ロガー取得
    const logger = c.get("logger");

    // ユースケース実行
    const result = useCase.execute(logger, input);

    return c.json(result);
};</code></pre>
</div>
<p><span style="color: #ff0000;">※サンプルAPIのハンドラー部分（ユースケースの呼び出し）</span></p>
<p>&nbsp;</p>
<p>・「src/presentation/http/supporting/sample/routes.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/supporting/sample/routes.ts"><code>import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
import type {
  SampleHelloUseCase,
  SampleTextInput,
  SampleTextUseCase,
} from "../../../../supporting/sample/useCase.js";
import {
  createSampleHelloHandler,
  createSampleTextHandler,
} from "./handler.js";

// リクエストボディ用のスキーマ定義
const sampleTextRequestBodySchema = z.object({
  text: z
    .string()
    .min(1, "textは必須です")
    .max(10, "textは10文字以内で入力してください"),
});


export const createSampleRoutes = (
  sampleHelloUseCase: SampleHelloUseCase,
  sampleTextUseCase: SampleTextUseCase,
) =&gt; {
  // Honoでルーティングを作る際は、メソッドチェーン推薦
  return (
    new Hono()
      // GET /sample
      .get("/", createSampleHelloHandler(sampleHelloUseCase))
      // POST /sample
      .post(
        "/",
        zValidator("json", sampleTextRequestBodySchema), // バリデーションチェック
        (c) =&gt; {
          // バリデーション済みのリクエストパラメータを取得
          const { text } = c.req.valid("json");

          // バリデーション済みのリクエストパラメータを取得
          const input: SampleTextInput = { text };

          return createSampleTextHandler(sampleTextUseCase)(c, input);
        },
      )
  );
};</code></pre>
</div>
<p><span style="color: #ff0000;">※サンプルAPIのルーティング部分（リクエストパラメータのバリデーションチェックを含む）、Honoでルーティングを作る際は、メソッドチェーン推薦</span></p>
<p>&nbsp;</p>
<p>・「src/presentation/http/supporting/sample/routes.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/supporting/sample/routes.test.ts"><code>import { Hono } from "hono";
import { testClient } from "hono/testing";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createApp } from "../../../../app.js";
import type { NodeConfig } from "../../../../runtime/node/config.js";
import { config } from "../../../../runtime/node/config.js";
import type {
  SampleHelloUseCase,
  SampleTextUseCase,
} from "../../../../supporting/sample/useCase.js";
import { createSampleRoutes } from "./routes.js";

// ログ出力の無効化
vi.spyOn(console, "log").mockImplementation(() =&gt; {});

// UseCaseのモック
let sampleHelloUseCase: {
  execute: ReturnType&lt;typeof vi.fn&gt;;
};

let sampleTextUseCase: {
  execute: ReturnType&lt;typeof vi.fn&gt;;
};

// テスト用クライアント作成関数
const createTestClient = (config: NodeConfig) =&gt; {
  // ルーティング設定
  const v1 = new Hono().route(
    "/sample",
    createSampleRoutes(
      sampleHelloUseCase as SampleHelloUseCase,
      sampleTextUseCase as SampleTextUseCase,
    ),
  );
  const routes = new Hono().route("/v1", v1);
  const app = createApp(config).route("/api", routes);

  return testClient(app);
};

// テスト用クライアント変数
let client: ReturnType&lt;typeof createTestClient&gt;;

// テスト用コンフィグ変数
let testConfig: NodeConfig;

// テスト前の共通セットアップ
beforeEach(() =&gt; {
  // モックの初期化
  sampleHelloUseCase = {
    execute: vi.fn().mockReturnValue({
      message: "Hello World !!",
    }),
  };

  sampleTextUseCase = {
    execute: vi.fn().mockReturnValue({
      message: "テキスト！",
    }),
  };

  testConfig = {
    ...config,
  };

  client = createTestClient(testConfig);
});

describe("GET /api/v1/sample", () =&gt; {
  it("正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.sample.$get();

    // 検証
    expect(response.status).toBe(200);
    expect(sampleHelloUseCase.execute).toHaveBeenCalledTimes(1);
  });
});

describe("POST /api/v1/sample", () =&gt; {
  it("正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.sample.$post({
      json: {
        text: "サンプル！",
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(sampleTextUseCase.execute).toHaveBeenCalledTimes(1);
    expect(sampleTextUseCase.execute).toHaveBeenCalledWith(expect.anything(), {
      text: "サンプル！",
    });
  });

  it("textが未指定の場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.sample.$post({
      json: {
        text: "",
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(sampleTextUseCase.execute).not.toHaveBeenCalled();
  });

  it("textが全角10文字以内の場合に正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.sample.$post({
      json: {
        text: "あいうえおかきくけこ",
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(sampleTextUseCase.execute).toHaveBeenCalledTimes(1);
  });

  it("textが全角10文字を超えている場合にバリデーションエラーになること", async () =&gt; {
     // テスト実行
     const response = await client.api.v1.sample.$post({
       json: {
         text: "あいうえおかきくけこさ",
       },
     });

     // 検証
     expect(response.status).toBe(400);
     expect(sampleTextUseCase.execute).not.toHaveBeenCalled();
  });

  it("textが半角10文字以内の場合に正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.sample.$post({
      json: {
        text: "abcdefghij",
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(sampleTextUseCase.execute).toHaveBeenCalledTimes(1);
  });

  it("textが半角10文字を超えている場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.sample.$post({
      json: {
        text: "abcdefghijk",
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(sampleTextUseCase.execute).not.toHaveBeenCalled();
  });
});</code></pre>
</div>
<p><span style="color: #ff0000;">※HTTPリクエスト部分のテストを行う。（主にバリデーションチェックの確認）</span></p>
<p>&nbsp;</p>
<p>・「src/presentation/http/middleware/requestLogger.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/middleware/requestLogger.ts"><code>import { createMiddleware } from "hono/factory";
import { logger } from "../../../infrastructure/logger/consoleLogger.js";

export const requestLogger = createMiddleware(async (c, next) =&gt; {
  // 処理の開始時刻を取得
  const start = performance.now();

  // Honoのミドルウェアで設定したリクエストIDを取得
  const requestId = c.get("requestId");

  // リクエストIDをセットしたロガーを取得
  const requestLogger = logger.withRequestId(requestId);

  // 共通ロガーを設定
  c.set("logger", requestLogger);

  // 処理実行
  await next();

  // 処理時間を取得
  const duration = performance.now() - start;

  // 処理結果をログ出力
  requestLogger.info("HTTP request", {
    method: c.req.method,
    path: c.req.path,
    durationMs: Math.round(duration),
    status: c.res.status,
  });
});</code></pre>
</div>
<p><span style="color: #ff0000;">※リクエスト単位で一意のリクエストIDを付与し、処理結果をログ出力するミドルウェア。</span></p>
<p>&nbsp;</p>
<p>・「src/app.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/app.ts"><code>import { Hono } from "hono";
import { cors } from "hono/cors";
import { requestId } from "hono/request-id";
import { requestLogger } from "./presentation/http/middleware/requestLogger.js";
import type { Logger } from "./shared/logger/logger.js";

// アプリのコンフィグ型
export type AppConfig = {
  env: string;
  corsOrigin: string;
};

// アプリの変数型
export type AppVariables = {
  Variables: {
    requestId: string;
    logger: Logger;
  };
};

export const createApp = (config: AppConfig) =&gt; {
  // Honoでルーティングを作る際は、メソッドチェーン推薦
  return (
    new Hono&lt;AppVariables&gt;()
      .get("/", (c) =&gt; {
        return c.text("Hello Hono!");
      })
      // CORS設定
      .use(
        "*",
        cors({
          origin: config.corsOrigin,
          allowHeaders: ["Content-Type", "Authorization"],
          credentials: true,
        }),
      )
      // 共通ミドルウェア設定
      .use("*", requestId())
      .use("*", requestLogger)
  );
};</code></pre>
</div>
<p><span style="color: #ff0000;">※アプリの共通定義。Honoでルーティングを作る際は、メソッドチェーン推薦。</span></p>
<p>&nbsp;</p>
<p>・「.env」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".env"><code>ENV=local
PORT=3000
CORS_ORIGIN="http://localhost:3000"</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル用の環境変数定義</span></p>
<p>&nbsp;</p>
<h3>Node.js環境用の各種ファイルを作成</h3>
<p>次にNode.js環境用に各種ファイルを作成するため、以下のコマンドを実行して各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/runtime/node
$ touch src/runtime/node/config.ts src/runtime/node/app.ts src/runtime/node/server.ts
$ mkdir -p src/di &amp;&amp; touch src/di/nodeContainer.ts
$ touch src/presentation/http/routesNode.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/runtime/node/config.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/runtime/node/config.ts"><code>import type { AppConfig } from "../../app.js";

// Node環境用のコンフィグ型
export type NodeConfig = AppConfig &amp; {
  port: number;
};

// Node環境用のコンフィグ設定
export const config: NodeConfig = {
  env: process.env.ENV ?? "local",
  corsOrigin: process.env.CORS_ORIGIN ?? "http://localhost:3000",
  port: Number(process.env.PORT ?? "3000"),
};

</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/runtime/node/app.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/runtime/node/app.ts"><code>import { createApp } from "../../app.js";
import { createNodeContainer } from "../../di/nodeContainer.js";
import { createRoutesNode } from "../../presentation/http/routesNode.js";
import type { NodeConfig } from "./config.js";

// Node.js環境用のapp作成関数
export const createNodeApp = (config: NodeConfig) =&gt; {
  const container = createNodeContainer(config);

  return createApp(config).route("/api", createRoutesNode(container));
};

export type NodeApp = ReturnType&lt;typeof createNodeApp&gt;;</code></pre>
</div>
<p><span style="color: #ff0000;">※appだけを作成する</span></p>
<p>&nbsp;</p>
<p>・「src/runtime/node/server.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/runtime/node/server.ts"><code>import { serve } from "@hono/node-server";
import { createNodeApp } from "./app.js";
import { config } from "./config.js";

// Node.js環境用のapp作成
const app = createNodeApp(config);

// サーバー起動処理
serve(
  {
    fetch: app.fetch,
    port: config.port,
  },
  (info) =&gt; {
    console.log(`Server is running on http://localhost:${info.port}`);
  },
);</code></pre>
</div>
<p><span style="color: #ff0000;">※サーバー起動処理を作成する</span></p>
<p>&nbsp;</p>
<p>・「src/di/nodeContainer.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/di/nodeContainer.ts"><code>import type { NodeConfig } from "../runtime/node/config.js";
import {
  SampleHelloUseCase,
  SampleTextUseCase,
} from "../supporting/sample/useCase.js";

export const createNodeContainer = (_config: NodeConfig) =&gt; {
  const sampleHelloUseCase = new SampleHelloUseCase();
  const sampleTextUseCase = new SampleTextUseCase();

  return {
    sampleHelloUseCase,
    sampleTextUseCase,
  };
};

export type NodeContainer = ReturnType&lt;typeof createNodeContainer&gt;;</code></pre>
</div>
<p><span style="color: #ff0000;">※パラメータのconfigはまだ使わないので「_config」としておく</span></p>
<p>&nbsp;</p>
<p>・「src/presentation/http/routesNode.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/routesNode.ts"><code>import { Hono } from "hono";
import type { NodeContainer } from "../../di/nodeContainer.js";
import { createSampleRoutes } from "../http/supporting/sample/routes.js";

export const createRoutesNode = (container: NodeContainer) =&gt; {
  /******************************
  * v1用のルーティング
  ******************************/
  // Honoでルーティングを作る際は、メソッドチェーン推薦
  const v1 = new Hono()
    .route(
      "/sample",
      createSampleRoutes(
        container.sampleHelloUseCase,
        container.sampleTextUseCase,
      ),
    );

  /******************************
  * ルーティング設定
  ******************************/
  return new Hono().route("/v1", v1);
};

</code></pre>
</div>
<p><span style="color: #ff0000;">※ランタイム毎にルーティングを設定できるようにしておく。また、 Honoでルーティングを作る際は、メソッドチェーン推薦。</span></p>
<p>&nbsp;</p>
<h3>package.jsonのscriptsを修正</h3>
<p>次にpackage.jsonのscriptsを以下のように修正します。</p>
<p>・「package.json」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="package.json"><code>{

・・・

  "scripts": {
    "dev": "pnpm run dev:node",
    "dev:node": "tsx watch src/runtime/node/server.ts",
    "build:node": "tsc",
    "start:node": "node dist/runtime/node/server.js",
    "test": "vitest run",
    "test:verbose": "vitest run --reporter=verbose",
    "test:watch": "vitest",
    "format": "biome format --write . &amp;&amp; biome check --write --only=assist/source/organizeImports .",
    "lint": "biome lint .",
    "check": "biome check .",
    "check:fix": "biome check --write ."
  },

・・・

}</code></pre>
</div>
<p>&nbsp;</p>
<h3>フォーマッターとリンターによるコードチェック</h3>
<p>次に以下のコマンドを実行し、フォーマッターとリンターによるコードチェックを行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm run format
$ pnpm run lint</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のように警告やエラーがでなければOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-8.png" alt="" width="1102" height="234" class="aligncenter size-full wp-image-20653" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-8.png 1102w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-8-300x64.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-8-1024x217.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-8-768x163.png 768w" sizes="auto, (max-width: 1102px) 100vw, 1102px" />
<p>&nbsp;</p>
<h3>テストコードによるチェック</h3>
<p>次にテストコードによるチェックを行うため、以下のコマンドを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm run test</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のように全てのテストがパスすればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-9.png" alt="" width="980" height="386" class="aligncenter size-full wp-image-20654" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-9.png 980w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-9-300x118.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-9-768x302.png 768w" sizes="auto, (max-width: 980px) 100vw, 980px" />
<p>&nbsp;</p>
<h3>サンプルAPIをPostmanで試す</h3>
<p>次にサンプルAPIをPostmanで試してみます。</p>
<p>まずは以下のコマンドを実行し、ローカルサーバーを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm run dev</code></pre>
</div>
<p>&nbsp;</p>
<p>次にGETメソッドで「http://localhost:3000/api/v1/sample」を実行し、以下のように正常終了して想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-10.png" alt="" width="2546" height="1582" class="aligncenter size-full wp-image-20657" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-10.png 2546w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-10-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-10-1024x636.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-10-768x477.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-10-1536x954.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-10-2048x1273.png 2048w" sizes="auto, (max-width: 2546px) 100vw, 2546px" />
<p>&nbsp;</p>
<p>次にPOSTメソッドで「http://localhost:3000/api/v1/sample」を実行し、以下のように正常終了して想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-11.png" alt="" width="2540" height="1578" class="aligncenter size-full wp-image-20658" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-11.png 2540w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-11-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-11-1024x636.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-11-768x477.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-11-1536x954.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-11-2048x1272.png 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>尚、共通ロガーも設定しているため、以下のようにリクエスト単位でログ出力も確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-12.png" alt="" width="2446" height="250" class="aligncenter size-full wp-image-20659" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-12.png 2446w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-12-300x31.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-12-1024x105.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-12-768x78.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-12-1536x157.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-12-2048x209.png 2048w" sizes="auto, (max-width: 2446px) 100vw, 2446px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>データベースの導入・設定</h2>
<p>次にデータベースを導入し、APIから操作するための各種準備を行います。</p>
<p>今回はまずNode.js環境用を想定し、データベースには<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「PostgreSQL」</strong></span>、ORMには<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「Drizzle ORM」</strong></span>を試してみます。</p>
<p><span style="color: #ff0000;">※Drizzle ORMは、強力な型安全性と軽量さを追求したSQLデータベース向けのモダンなTypeScript用ORMライブラリです。</span></p>
<p>また、データベースはDocker環境を利用して構築するため、後述の内容を試したい場合は、事前に<a href="https://www.docker.com/ja-jp/products/docker-desktop/" target="_blank" rel="noopener">Docker Desktop</a>などの準備を行なって下さい。</p>
<p>&nbsp;</p>
<h3>PostgreSQLのDockerコンテナ準備</h3>
<p>まずはPostgreSQLのDockerコンテナを準備するため、以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p deploy/docker/local/db/pg &amp;&amp; touch deploy/docker/local/db/pg/Dockerfile
$ mkdir -p deploy/docker/local/db/pg/init &amp;&amp; touch deploy/docker/local/db/pg/init/init.sql
$ touch compose.yaml</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「deploy/docker/local/db/pg/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="deploy/docker/local/db/pg/Dockerfile"><code>FROM postgres:18.6

ENV LANG=ja_JP.utf8

# PostgreSQLの日本語化で「ja_JP.utf8」を使うために必要
RUN apt-get update &amp;&amp; \
    apt-get install -y locales &amp;&amp; \
    rm -rf /var/lib/apt/lists/* &amp;&amp; \
    localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8</code></pre>
</div>
<p>&nbsp;</p>
<p>・「deploy/docker/local/db/pg/init/init.sql」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-sql" data-lang="SQL" data-file="deploy/docker/local/db/pg/init/init.sql"><code>-- テスト用DB作成
CREATE DATABASE "testing-db-pg";</code></pre>
</div>
<p>&nbsp;</p>
<p>・「compose.yaml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="compose.yaml"><code>services:
  pg:
    container_name: hono-sample-pg
    build:
      context: .
      dockerfile: ./deploy/docker/local/db/pg/Dockerfile
    environment:
      POSTGRES_DB: "local-db-pg"
      POSTGRES_USER: "root"
      POSTGRES_PASSWORD: "root-pass"
      # ローカル環境でもパスワードを有効化する設定
      POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256
    volumes:
      - hono-sample-pg-data:/var/lib/postgresql
      # 初回起動時にSQLを実行する設定
      - ./deploy/docker/local/db/pg/init:/docker-entrypoint-initdb.d
    ports:
      - "5432:5432"
volumes:
  hono-sample-pg-data:</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「.env」を以下のように修正します。</p>
<p>・「.env」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".env"><code>ENV=production
PORT=3000
CORS_ORIGIN="http://localhost:3000"
DB_PG_NAME=local-db-pg
DB_PG_USER=root
DB_PG_PASSWORD=root-pass
DB_PG_HOST=localhost
DB_PG_PORT=5432
DB_PG_MAX_OPEN_CONNS=2
DB_PG_CONN_MAX_LIFETIME=300</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ステータスを確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose ps</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のようにDockerコンテナが起動すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-13.png" alt="" width="1784" height="98" class="aligncenter size-full wp-image-20669" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-13.png 1784w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-13-300x16.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-13-1024x56.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-13-768x42.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-13-1536x84.png 1536w" sizes="auto, (max-width: 1784px) 100vw, 1784px" />
<p>&nbsp;</p>
<h3>Drizzle ORMを利用する準備</h3>
<p>次にDrizzle ORMを利用する準備をするため、以下のコマンドを実行し、各種ライブラリをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add drizzle-orm pg dotenv
$ pnpm add -D drizzle-kit @types/pg</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch drizzle.pg.config.ts
$ mkdir -p src/infrastructure/database/drizzle/pg &amp;&amp; touch src/infrastructure/database/drizzle/pg/client.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「drizzle.pg.config.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="drizzle.pg.config.ts"><code>import "dotenv/config";
import { defineConfig } from "drizzle-kit";

// テスト用のDB名
const testingDbPgName = "testing-db-pg";

// 接続先DB名
const dbPgName =
  process.env.ENV === "testing" ? testingDbPgName : process.env.DB_PG_NAME;

// 接続先URL
const url =
  `postgresql://${process.env.DB_PG_USER}:${process.env.DB_PG_PASSWORD}` +
  `@${process.env.DB_PG_HOST}:${process.env.DB_PG_PORT}` +
  `/${dbPgName}`;

// drizzleのDB接続設定
export default defineConfig({
  dialect: "postgresql",
  schema: "./src/infrastructure/database/drizzle/pg/schema/*.ts",
  out: "./src/infrastructure/database/drizzle/pg/migrations",
  dbCredentials: {
    url: url,
  },
});</code></pre>
</div>
<p><span style="color: #ff0000;">※drizzle-kit用の設定ファイル</span></p>
<p>&nbsp;</p>
<p>・「src/infrastructure/database/drizzle/pg/client.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/infrastructure/database/drizzle/pg/client.ts"><code>import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";

// PostgreSQL用のコンフィグ型
export type PgConfig = {
  name: string;
  user: string;
  password: string;
  host: string;
  port: number;
  maxOpenConns: number;
  connMaxLifetime: number;
};

// PostgreSQLのインスタンス型
export type PgDatabase = NodePgDatabase;

// PostgreSQLのトランザクションのインスタンス型
export type PgTransaction = Parameters&lt;
  Parameters&lt;PgDatabase["transaction"]&gt;[0]
&gt;[0];

// PostgreSQLのDBクライアント型
export type PgDbClient = PgDatabase | PgTransaction;

// DBインスタンス生成用関数
export const createDbPg = (config: PgConfig) =&gt; {
  const pool = new Pool({
    database: config.name,
    user: config.user,
    password: config.password,
    host: config.host,
    port: config.port,
    max: config.maxOpenConns,
    maxLifetimeSeconds: config.connMaxLifetime,
  });

  const db = drizzle({
    client: pool,
  });

  return {
    pool,
    db,
  };
};

export type DbPg = ReturnType&lt;typeof createDbPg&gt;;</code></pre>
</div>
<p><span style="color: #ff0000;">※テストコードなどでPostgreSQLインスタンスを止めたい場合は、「pool.end()」を利用するため、戻り値に「pool」も入れている。</span></p>
<p>&nbsp;</p>
<p>次にファイル「tsconfig.json」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="tsconfig.json"><code>{
  ・・・

  "exclude": ["node_modules", "drizzle.pg.config.ts"]
}</code></pre>
</div>
<p><span style="color: #ff0000;">※excludeに「drizzle.pg.config.ts」を追加</span></p>
<p>&nbsp;</p>
<h3>drizzle-kitを利用してusersテーブルを作ってみる</h3>
<p>次に後述での利用も想定し、drizzle-kitを利用してusersテーブルを作ってみます。</p>
<p>まずは以下のコマンドを実行し、usersテーブル用のスキーマファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/infrastructure/database/drizzle/pg/schema &amp;&amp; touch src/infrastructure/database/drizzle/pg/schema/users.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「src/infrastructure/database/drizzle/pg/schema/users.ts」を以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/infrastructure/database/drizzle/pg/schema/users.ts"><code>import {
  bigint,
  index,
  integer,
  pgTable,
  text,
  timestamp,
  uuid,
} from "drizzle-orm/pg-core";

export const users = pgTable(
  "users",
  {
    id: bigint("id", { mode: "number" })
      .primaryKey()
      .generatedAlwaysAsIdentity(),
    uid: uuid().notNull().defaultRandom().unique(),
    lastName: text("last_name").notNull(),
    firstName: text("first_name").notNull(),
    totalPurchaseAmount: integer("total_purchase_amount").notNull().default(0),
    createdAt: timestamp("created_at", {
      withTimezone: true,
    })
      .notNull()
      .defaultNow(),
    updatedAt: timestamp("updated_at", {
      withTimezone: true,
    })
      .notNull()
      .defaultNow(),
  },
  (table) =&gt; [index("users_last_name_idx").on(table.lastName)],
);</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、マイグレーション用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm drizzle-kit generate --config=drizzle.pg.config.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、ファイル「src/infrastructure/database/drizzle/pg/migrations/0000_lean_sabretooth.sql」が作成されればOKです。</p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、マイグレーションを行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm drizzle-kit migrate --config=drizzle.pg.config.ts</code></pre>
</div>
<p><span style="color: #ff0000;">※テスト用DBへ実行したい場合は、「ENV=testing pnpm drizzle-kit migrate &#8211;config=drizzle.pg.config.ts」を利用して下さい。尚、ロールバック用のコマンドはないため、適用済みのマイグレーションを戻したい場合は、手動での対応（打ち消し用のSQLを作って実行する）が必要になります。</span></p>
<p>&nbsp;</p>
<p>実行後、以下のように正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-14.png" alt="" width="1394" height="124" class="aligncenter size-full wp-image-20670" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-14.png 1394w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-14-300x27.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-14-1024x91.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-14-768x68.png 768w" sizes="auto, (max-width: 1394px) 100vw, 1394px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、DBに接続してテーブル一覧を確認してみます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>docker compose exec pg bash
PGPASSWORD=root-pass psql -U root -d local-db-pg
\dt</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のようにusersテーブルが作成されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-15.png" alt="" width="954" height="334" class="aligncenter size-full wp-image-20671" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-15.png 954w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-15-300x105.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-15-768x269.png 768w" sizes="auto, (max-width: 954px) 100vw, 954px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テーブル定義を確認してみます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>\d users</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のようにusersテーブルの定義が確認できればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-16.png" alt="" width="1344" height="434" class="aligncenter size-full wp-image-20673" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-16.png 1344w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-16-300x97.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-16-1024x331.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/09/hono-sample-16-768x248.png 768w" sizes="auto, (max-width: 1344px) 100vw, 1344px" />
<p>&nbsp;</p>
<p>尚、DBから抜ける場合は、以下のコマンドを実行して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>\q
exit</code></pre>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Cloudflare環境でD1を使う想定の場合について</h2>
<p>上記では、Node.js環境かつデータベースに「PostgreSQL」を利用する想定で準備しましたが、<span style="color: #3366ff;"><strong>Honoはマルチランタイムに対応</strong></span>しており、<span style="color: #ff0000;"><strong>Cloudflare環境かつデータベースに「Cloudflare D1（SQLiteベース）」を利用したい方もいる</strong></span>と思います。</p>
<p>その場合は、以下の手順にてCloudflare環境向けのローカル開発環境へ対応させることが可能です。</p>
<p>&nbsp;</p>
<h3>Cloudflare環境用のCLIツール「wrangler」をインストール</h3>
<p>まずはCloudflare環境用のCLIツールをインストールするため、以下のコマンドを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add -D wrangler @cloudflare/workers-types</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、<strong><span style="color: #ff0000;">workerdに関する警告がでる</span></strong>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>コマンド「pnpm approve-builds」を実行し、スペースキーをクリックしてworkerdにチェックを付け、Enterで実行して承認</strong></span>して下さい。</p>
<p>承認後、ファイル「pnpm-workspace.yaml」のallowBuildsに「workerd: true」が追加されていればOKです。</p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、wranglerコマンドが使えることを確認して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm wrangler --version</code></pre>
</div>
<p>&nbsp;</p>
<h3>ローカル環境用のDB「D1（SQLiteベース）」を作成</h3>
<p>次にローカル環境用のデータベース「D1（SQLiteベース）」を作成するため、以下のコマンドを実行して、wrangler用の設定ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch wrangler.jsonc</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「wrangler.jsonc」を以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="wrangler.jsonc"><code>{
  "name": "local-d1",
  "compatibility_date": "2026-09-29",

  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "local-db-d1",
      "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "migrations_dir": "src/infrastructure/database/drizzle/d1/migrations"
    }
  ]
}</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ローカル環境用のD1を作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm wrangler d1 execute local-db-d1 --local --command="SELECT 1"</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル環境用へ実行する場合はオプション「&#8211;local」必須です。</span></p>
<p>&nbsp;</p>
<p>実行後、ディレクトリ「.wrangler」配下に拡張子「.sqlite」の以下のようなファイル「.wrangler/state/v3/d1/miniflare-D1DatabaseObject/423c60534c20823e3775c6e1de8b0d6f1fb85ef14b525413932d985d8dc6abf5.sqlite」が作成されるため、ファイルパスをメモします。</p>
<p>&nbsp;</p>
<p>次にテスト用にもう一つ作りたい場合は、ファイル「wrangler.jsonc」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="wrangler.jsonc"><code>{
  "name": "local-d1",
  "compatibility_date": "2026-09-29",

  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "local-db-d1",
      "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "migrations_dir": "src/infrastructure/database/drizzle/d1/migrations"
    },
    {
      "binding": "TEST_DB",
      "database_name": "testing-db-d1",
      "database_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
      "migrations_dir": "src/infrastructure/database/drizzle/d1/migrations"
    }
  ]
}</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テスト用のD1も作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm wrangler d1 execute testing-db-d1 --local --command="SELECT 1"</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル環境用へ実行する場合はオプション「&#8211;local」必須です。</span></p>
<p>&nbsp;</p>
<p>実行後、先ほどとは違うファイル名の「.wrangler/state/v3/d1/miniflare-D1DatabaseObject/d5cace998a99b013073f98823ec85d6a6601f80941c4f243f809eef878b809e8.sqlite」が作成されるため、ファイルパスをメモします。</p>
<p>&nbsp;</p>
<p>次にファイル「.env」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".env"><code>ENV=production
PORT=3000
CORS_ORIGIN="http://localhost:3000"
DB_PG_NAME=local-db-pg
DB_PG_USER=root
DB_PG_PASSWORD=root-pass
DB_PG_HOST=localhost
DB_PG_PORT=5432
DB_PG_MAX_OPEN_CONNS=2
DB_PG_CONN_MAX_LIFETIME=300
DB_D1_PATH=".wrangler/state/v3/d1/miniflare-D1DatabaseObject/423c60534c20823e3775c6e1de8b0d6f1fb85ef14b525413932d985d8dc6abf5.sqlite"
DB_D1_TEST_PATH=".wrangler/state/v3/d1/miniflare-D1DatabaseObject/d5cace998a99b013073f98823ec85d6a6601f80941c4f243f809eef878b809e8.sqlite"</code></pre>
</div>
<p><span style="color: #ff0000;">※DB_D1_PATHとDB_D1_TEST_PATHの値は各自メモした値を設定して下さい。</span></p>
<p>&nbsp;</p>
<p>次にディレクトリ「.wrangle」はgit管理対象外のため、ファイル「.gitignore」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".gitignore"><code>・・・

# wrangler
.wrangler/</code></pre>
</div>
<p>&nbsp;</p>
<h3>Drizzle ORMを利用する準備</h3>
<p>次に上記と同様にDrizzle ORMを利用しますが、<strong><span style="color: #ff0000;">現在通常インストールされるDrizzle ORMのバージョンがv1未満（今回は「0.45.3」でした）の場合、別途sqlite用のライブラリを利用する必要があるよう</span></strong>なので、以下のコマンドを実行し、追加で必要になるライブラリをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add -D node-gyp @types/better-sqlite3
$ pnpm add better-sqlite3</code></pre>
</div>
<p><span style="color: #ff0000;">※better-sqlite3を利用するには「node-gyp」も使うようです。</span></p>
<p>&nbsp;</p>
<p>実行後、<strong><span style="color: #ff0000;">better-sqlite3に関する警告</span></strong>がでるため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>コマンド「pnpm approve-builds」を実行し、スペースキーをクリックしてbetter-sqlite3にチェックを付け、Enterで実行して承認</strong></span>して下さい。</p>
<p>承認後、ファイル「pnpm-workspace.yaml」のallowBuildsに「better-sqlite3: true」が追加されていればOKです。</p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch drizzle.d1.config.ts
$ mkdir -p src/infrastructure/database/drizzle/d1 &amp;&amp; touch src/infrastructure/database/drizzle/d1/client.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「drizzle.d1.config.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="drizzle.d1.config.ts"><code>import "dotenv/config";
import { defineConfig } from "drizzle-kit";

// 接続先DBパス
const dbPath =
  process.env.ENV === "testing"
    ? process.env.DB_D1_TEST_PATH
    : process.env.DB_D1_PATH;

// 接続先URL
const url = `${dbPath}`;

// drizzleのDB接続設定
export default defineConfig({
  dialect: "sqlite",
  schema: "./src/infrastructure/database/drizzle/d1/schema/*.ts",
  out: "./src/infrastructure/database/drizzle/d1/migrations",
  dbCredentials: {
    url: url,
  },
});</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/infrastructure/database/drizzle/d1/client.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/infrastructure/database/drizzle/d1/client.ts"><code>import type { DrizzleD1Database } from "drizzle-orm/d1";
import { drizzle } from "drizzle-orm/d1";

// DBインスタンスの型
export type DatabaseD1 = DrizzleD1Database;

// DBクライアントの型
export type DbClient = DatabaseD1;

// DBインスタンス生成用関数
export const createDbD1 = (db: D1Database): DatabaseD1 =&gt; {
  return drizzle(db);
};</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「tsconfig.json」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="tsconfig.json"><code>{
  "compilerOptions": {

    ・・・

    "types": ["node", "@cloudflare/workers-types"],
  
    ・・・

  },
  "exclude": ["node_modules", "drizzle.pg.config.ts", "drizzle.d1.config.ts"]
}</code></pre>
</div>
<p><span style="color: #ff0000;">※compilerOptionsのtypesに「@cloudflare/workers-types」を追加、excludeに「drizzle.d1.config.ts」を追加</span></p>
<p>&nbsp;</p>
<h3>drizzle-kitを利用してusersテーブルを作ってみる</h3>
<p>次にdrizzle-kitを利用してusersテーブル作ってみます。</p>
<p>まずは以下のコマンドを実行し、usersテーブル用のスキーマファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/infrastructure/database/drizzle/d1/schema &amp;&amp; touch src/infrastructure/database/drizzle/d1/schema/users.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイル「src/infrastructure/database/drizzle/d1/schema/users.ts」を以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/infrastructure/database/drizzle/d1/schema/users.ts"><code>import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const users = sqliteTable(
  "users",
  {
    id: integer("id").primaryKey({ autoIncrement: true }),
    uid: text("uid").notNull().unique(),
    lastName: text("last_name").notNull(),
    firstName: text("first_name").notNull(),
    totalPurchaseAmount: integer("total_purchase_amount").notNull().default(0),
    createdAt: integer("created_at", {
      mode: "timestamp",
    })
      .notNull()
      .$defaultFn(() =&gt; new Date()),
    updatedAt: integer("updated_at", {
      mode: "timestamp",
    })
      .notNull()
      .$defaultFn(() =&gt; new Date()),
  },
  (table) =&gt; [index("users_last_name_idx").on(table.lastName)],
);</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、マイグレーション用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm drizzle-kit generate --config=drizzle.d1.config.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、ファイル「src/infrastructure/database/drizzle/d1/migrations/0000_public_invisible_woman.sql」が作成されればOKです。</p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、マイグレーションを行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm drizzle-kit migrate --config=drizzle.d1.config.ts</code></pre>
</div>
<p><span style="color: #ff0000;">※テスト用DBへ実行したい場合は、「ENV=testing pnpm drizzle-kit migrate &#8211;config=drizzle.d1.config.ts」を利用して下さい。</span></p>
<p>&nbsp;</p>
<p>実行後、以下のように正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-17.png" alt="" width="996" height="128" class="aligncenter size-full wp-image-20676" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-17.png 996w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-17-300x39.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-17-768x99.png 768w" sizes="auto, (max-width: 996px) 100vw, 996px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ローカルのD1に対してクエリを実行し、テーブル一覧を確認してみます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm wrangler d1 execute local-db-d1 --local --command="SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のようにusersテーブルが作成されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-18.png" alt="" width="1890" height="618" class="aligncenter size-full wp-image-20677" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-18.png 1890w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-18-300x98.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-18-1024x335.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-18-768x251.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-18-1536x502.png 1536w" sizes="auto, (max-width: 1890px) 100vw, 1890px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テーブル定義を確認してみます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm wrangler d1 execute local-db-d1 --local --command="PRAGMA table_info(users);"</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のようにusersテーブルの定義が確認できればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-19.png" alt="" width="1430" height="788" class="aligncenter size-full wp-image-20678" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-19.png 1430w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-19-300x165.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-19-1024x564.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-19-768x423.png 768w" sizes="auto, (max-width: 1430px) 100vw, 1430px" />
<p>&nbsp;</p>
<h3>Cloudflare環境用に対応し、wranglerでサーバー起動を試す</h3>
<p>次に上記で作成済みのAPIをCloudflare環境用に対応させてみます。</p>
<p>まずは以下のコマンドを実行し、TypeScriptの型定義を生成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm wrangler types</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、ファイル「worker-configuration.d.ts」が作成されればOKです。</p>
<p>次にbiomeのチェックから外すため、ファイル「biome.json」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="biome.json"><code>{

・・・

  "files": {
    "ignoreUnknown": false,
    "includes": ["**", "!**/worker-configuration.d.ts"]
  },

・・・

}</code></pre>
</div>
<p><span style="color: #ff0000;">※「&#8221;includes&#8221;: [&#8220;**&#8221;, &#8220;!**/worker-configuration.d.ts&#8221;]」を追加</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ランタイム用の各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/runtime/cloudflare
$ touch src/runtime/cloudflare/config.ts src/runtime/cloudflare/app.ts src/runtime/cloudflare/worker.ts
$ touch src/di/cloudflareContainer.ts
$ touch src/presentation/http/routesCloudflare.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/runtime/cloudflare/config.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/runtime/cloudflare/config.ts"><code>import type { AppConfig } from "../../app.js";

// Cloudflare環境用のコンフィグ定義
export type CloudflareConfig = AppConfig &amp; {
  db: D1Database;
};

// Cloudflare環境用のコンフィグ設定関数
export const createConfig = (env: Env): CloudflareConfig =&gt; {
  return {
    env: env.ENV,
    corsOrigin: env.CORS_ORIGIN,
    db: env.ENV === "testing" ? env.TEST_DB : env.DB,
  };
};</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/runtime/cloudflare/app.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript"><code>import { createApp } from "../../app.js";
import { createCloudflareContainer } from "../../di/cloudflareContainer.js";
import { createRoutesCloudflare } from "../../presentation/http/routesCloudflare.js";
import type { CloudflareConfig } from "./config.js";

// Cloudflare環境用のapp作成関数
export const createCloudflareApp = (config: CloudflareConfig) =&gt; {
  const container = createCloudflareContainer(config);

  return createApp(config).route("/api", createRoutesCloudflare(container));
};

export type CloudflareApp = ReturnType&lt;typeof createCloudflareApp&gt;;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/runtime/cloudflare/worker.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/runtime/cloudflare/worker.ts"><code>import { createCloudflareApp } from "./app.js";
import { createConfig } from "./config.js";

// サーバー起動処理
export default {
  fetch: (request: Request, env: Env, ctx: ExecutionContext) =&gt; {
    // コンフィグ作成
    const config = createConfig(env);

    // Cloudflare用のapp作成
    const app = createCloudflareApp(config);

    // 戻り値
    return app.fetch(request, env, ctx);
  },
};</code></pre>
</div>
<p><span style="color: #ff0000;">※サーバー起動後、「env: Env」が使えるようになるため、これを「createConfig」に渡してコンフィグを作成しています。</span></p>
<p>&nbsp;</p>
<p>・「src/di/cloudflareContainer.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/di/cloudflareContainer.ts"><code>import type { CloudflareConfig } from "../runtime/cloudflare/config.js";
import {
  SampleHelloUseCase,
  SampleTextUseCase,
} from "../supporting/sample/useCase.js";

export const createCloudflareContainer = (_config: CloudflareConfig) =&gt; {
  const sampleHelloUseCase = new SampleHelloUseCase();
  const sampleTextUseCase = new SampleTextUseCase();

  return {
    sampleHelloUseCase,
    sampleTextUseCase,
  };
};

export type CloudflareContainer = ReturnType&lt;typeof createCloudflareContainer&gt;;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/presentation/http/routesCloudflare.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/routesCloudflare.ts"><code>import { Hono } from "hono";
import type { CloudflareContainer } from "../../di/cloudflareContainer.js";
import { createSampleRoutes } from "../http/supporting/sample/routes.js";

export const createRoutesCloudflare = (container: CloudflareContainer) =&gt; {
  /******************************
  * v1用のルーティング
  ******************************/
  // Honoでルーティングを作る際は、メソッドチェーン推薦
  const v1 = new Hono().route(
    "/sample",
    createSampleRoutes(
      container.sampleHelloUseCase,
      container.sampleTextUseCase,
    ),
  );
  /******************************
  * ルーティング設定
  ******************************/
  return new Hono().route("/v1", v1);
};</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ローカルサーバーを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm wrangler dev</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、ローカルサーバー「http://localhost:8787/」が起動します。</p>
<p>Postmanで「GET http://localhost:8787/api/v1/sample」を実行し、以下のように想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-20.png" alt="" width="2540" height="1576" class="aligncenter size-full wp-image-20689" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-20.png 2540w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-20-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-20-1024x635.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-20-768x477.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-20-1536x953.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-20-2048x1271.png 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>尚、ログ出力も同様に出力されます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-21.png" alt="" width="996" height="236" class="aligncenter size-full wp-image-20690" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-21.png 996w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-21-300x71.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-21-768x182.png 768w" sizes="auto, (max-width: 996px) 100vw, 996px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>usersテーブル用のCRUD APIを作成する</h2>
<p>上記で準備したPostgreSQLのusersテーブルを利用し、CRUD APIを作成してみます。</p>
<p><strong><span style="border-bottom: 2px solid #be3144;">単純なCRUD処理を想定し、一般的な業務領域として「generic」ディレクトリを作成して各種ファイルを作ります。</span></strong></p>
<p>&nbsp;</p>
<h3>カスタムエラー用の設定</h3>
<p>まずはカスタムエラーに対応できるようにするため、以下のコマンドを実行して各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/shared/error &amp;&amp; touch src/shared/error/appError.ts
$ mkdir -p src/presentation/http/error
$ touch src/presentation/http/error/errorHandler.ts src/presentation/http/error/errorStatusMap.ts
$ mkdir -p src/generic/user &amp;&amp; touch src/generic/user/errors.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/shared/error/appError.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/shared/error/appError.ts"><code>// ログレベル
export type LogLevel = "info" | "warn" | "error" | "debug";

// 共通エラークラス
export class AppError&lt;TCode extends string = string&gt; extends Error {
  constructor(
    message: string,
    public readonly code: TCode,
    public readonly logLevel: LogLevel = "info",
  ) {
    super(message);
    this.name = "AppError";
  }
}
</code></pre>
</div>
<p><span style="color: #ff0000;">※ジェネリクスでカスタムエラーコード用の型を指定できるようにしている。</span></p>
<p>&nbsp;</p>
<p>・「src/presentation/http/error/errorHandler.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/error/errorHandler.ts"><code>import type { ErrorHandler } from "hono";
import { AppError } from "../../../shared/error/appError.js";
import { errorStatusMap } from "./errorStatusMap.js";

export const errorHandler: ErrorHandler = (err, c) =&gt; {
  const logger = c.get("logger");

  // カスタムエラー用のエラーハンドリング
  // 共通エラークラスを利用してカスタムエラークラスを作成し、
  // エラーコードとHTTPステータスコードのマッピングを行う
  if (err instanceof AppError) {
    const status = errorStatusMap[err.code];

    // エラーコードに対応するHTTPステータスコードが存在する場合は、エラーレスポンスを返す
    if (status) {
      // ログ出力
      logger[err.logLevel]("AppError occurred", {
        error: err,
        requestId: c.get("requestId"),
      });

      return c.json(
        {
          code: err.code,
          message: err.message,
        },
        status,
      );
    }
  }

  logger.error("Unexpected error", {
    error: err,
  });

  return c.json(
    {
      message: "Internal Server Error",
    },
    500,
  );
};</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/presentation/http/error/errorStatusMap.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/error/errorStatusMap.ts"><code>import type { ContentfulStatusCode } from "hono/utils/http-status";
import { userErrorStatusMap } from "../../../generic/user/errors.js";

// 共通エラーコードとHTTPステータスコードのマッピング
export const errorStatusMap: Record&lt;string, ContentfulStatusCode&gt; = {
  ...userErrorStatusMap,
};
</code></pre>
</div>
<p><span style="color: #ff0000;">※エラーコードを追加したらここでまとめる</span></p>
<p>&nbsp;</p>
<p>・「src/generic/user/errors.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/generic/user/errors.ts"><code>import type { ContentfulStatusCode } from "hono/utils/http-status";
import { AppError } from "../../shared/error/appError.js";

// エラーコード（カスタムエラー用）
// 「|」で区切って追加可能
export type UserErrorCode = "USER_NOT_FOUND";

// エラーコードとHTTPステータスコードのマッピング
export const userErrorStatusMap: Record&lt;UserErrorCode, ContentfulStatusCode&gt; = {
  USER_NOT_FOUND: 404,
};

export class UserNotFoundError extends AppError&lt;UserErrorCode&gt; {
  constructor() {
    super("User not found", "USER_NOT_FOUND", "warn");
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※カスタムエラーを作る場合の例</span></p>
<p>&nbsp;</p>
<p>次にファイル「src/app.ts」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/app.ts"><code>import { Hono } from "hono";
import { cors } from "hono/cors";
import { requestId } from "hono/request-id";
import { errorHandler } from "./presentation/http/error/errorHandler.js";
import { requestLogger } from "./presentation/http/middleware/requestLogger.js";
import type { Logger } from "./shared/logger/logger.js";

// アプリのコンフィグ型
export type AppConfig = {
  env: string;
  corsOrigin: string;
};

// アプリの変数型
export type AppVariables = {
  Variables: {
    requestId: string;
    logger: Logger;
  };
};

export const createApp = (config: AppConfig) =&gt; {
  // Honoでルーティングを作る際は、メソッドチェーン推薦
  return (
    new Hono&lt;AppVariables&gt;()
      .get("/", (c) =&gt; {
        return c.text("Hello Hono!");
      })
      // エラーハンドラー設定
      .onError(errorHandler)
      // CORS設定
      .use(
        "*",
        cors({
          origin: config.corsOrigin,
          allowHeaders: ["Content-Type", "Authorization"],
          credentials: true,
        }),
      )
      // 共通ミドルウェア設定
      .use("*", requestId())
      .use("*", requestLogger)
  );
};</code></pre>
</div>
<p><span style="color: #ff0000;">※「app.onError(errorHandler);」を追加してカスタムエラーを使えるようにする</span></p>
<p>&nbsp;</p>
<h3>DB接続用のコンフィグ設定を追加</h3>
<p>次にDB接続用のコンフィグ設定を追加するため、ファイル「src/runtime/node/config.ts」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/runtime/node/config.ts"><code>import type { AppConfig } from "../../app.js";
import type { PgConfig } from "../../infrastructure/database/drizzle/pg/client.js";

// Node環境用のコンフィグ型
export type NodeConfig = AppConfig &amp; {
  port: number;
  db: PgConfig;
};

// Node環境用のコンフィグ設定
export const config: NodeConfig = {
  env: process.env.ENV ?? "local",
  corsOrigin: process.env.CORS_ORIGIN ?? "http://localhost:3000",
  port: Number(process.env.PORT ?? "3000"),
  db: {
    name: process.env.DB_PG_NAME ?? "local-db-pg",
    user: process.env.DB_PG_USER ?? "root",
    password: process.env.DB_PG_PASSWORD ?? "root-pass",
    host: process.env.DB_PG_HOST ?? "localhost",
    port: Number(process.env.DB_PG_PORT ?? "5432"),
    maxOpenConns: Number(process.env.DB_PG_MAX_OPEN_CONNS ?? "2"),
    connMaxLifetime: Number(process.env.DB_PG_CONN_MAX_LIFETIME ?? "300"),
  },
};</code></pre>
</div>
<p><span style="color: #ff0000;">※ランタイム環境に合わせた環境変数設定をする</span></p>
<p>&nbsp;</p>
<h3>ユーザー用のCRUD APIを追加</h3>
<p>次にユーザー用のCRUD APIを追加するため、以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch src/generic/user/useCase.ts src/generic/user/useCase.test.ts
$ mkdir -p src/presentation/http/generic/user
$ touch src/presentation/http/generic/user/handler.ts
$ touch src/presentation/http/generic/user/routes.ts src/presentation/http/generic/user/routes.test.ts
$ mkdir -p src/tests/integration/generic/user &amp;&amp; touch src/tests/integration/generic/user/user.test.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/generic/user/useCase.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/generic/user/useCase.ts"><code>import { eq } from "drizzle-orm";
import type { PgDatabase } from "../../infrastructure/database/drizzle/pg/client.js";
import { users } from "../../infrastructure/database/drizzle/pg/schema/users.js";
import type { Logger } from "../../shared/logger/logger.js";
import { UserNotFoundError } from "./errors.js";

export type CreateUserInput = {
  lastName: string;
  firstName: string;
  totalPurchaseAmount: number;
};

export type FindUserInput = {
  uid: string;
};

export type UpdateUserInput = {
  uid: string;
  lastName: string;
  firstName: string;
  totalPurchaseAmount: number;
};

export type DeleteUserInput = {
  uid: string;
};

// ユーザー作成
export class CreateUserUseCase {
  constructor(private readonly db: PgDatabase) {}

  async execute(logger: Logger, input: CreateUserInput) {
    logger.info("Create user", input);

    const [user] = await this.db
      .insert(users)
      .values({
        lastName: input.lastName,
        firstName: input.firstName,
        totalPurchaseAmount: input.totalPurchaseAmount,
      })
      .returning();

    return user;
  }
}

// 全てのユーザー取得
export class FindUsersUseCase {
  constructor(private readonly db: PgDatabase) {}

  async execute(logger: Logger) {
    logger.info("Find users");

    const result = await this.db.select().from(users);

    return result;
  }
}

// 対象のユーザー取得
export class FindUserUseCase {
  constructor(private readonly db: PgDatabase) {}

  async execute(logger: Logger, input: FindUserInput) {
    logger.info("Find user", input);

    const [user] = await this.db
      .select()
      .from(users)
      .where(eq(users.uid, input.uid))
      .limit(1);

      return user ?? {};
  }
}

// 対象のユーザー更新
export class UpdateUserUseCase {
  constructor(private readonly db: PgDatabase) {}

  async execute(logger: Logger, input: UpdateUserInput) {
    logger.info("Update user", input);

    const user = await this.db.transaction(async (tx) =&gt; {
      const [user] = await tx
        .update(users)
        .set({
          lastName: input.lastName,
          firstName: input.firstName,
          totalPurchaseAmount: input.totalPurchaseAmount,
          updatedAt: new Date(),
        })
        .where(eq(users.uid, input.uid))
        .returning();

      if (!user) {
        throw new UserNotFoundError();
      }

      return user;
    });

    return user;
  }
}

// 対象のユーザー削除
export class DeleteUserUseCase {
  constructor(private readonly db: PgDatabase) {}

  async execute(logger: Logger, input: DeleteUserInput) {
    logger.info("Delete user", input);

    const [user] = await this.db
      .delete(users)
      .where(eq(users.uid, input.uid))
      .returning();

    if (!user) {
      throw new UserNotFoundError();
    }

    return user;
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※トランザクションを使う場合は、ユースケース単位で使うことを想定としている</span></p>
<p>&nbsp;</p>
<p>・「src/generic/user/useCase.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/generic/user/useCase.test.ts"><code>import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PgDatabase } from "../../infrastructure/database/drizzle/pg/client.js";
import type { Logger } from "../../shared/logger/logger.js";
import { UserNotFoundError } from "./errors.js";
import {
  CreateUserUseCase,
  DeleteUserUseCase,
  FindUsersUseCase,
  FindUserUseCase,
  UpdateUserUseCase,
} from "./useCase.js";

// ロガーのモック化
const createMockLogger = (): Logger =&gt; ({
  info: vi.fn(),
  warn: vi.fn(),
  error: vi.fn(),
  debug: vi.fn(),
  withRequestId: vi.fn(),
});

// DBのモック化
const createMockDb = &lt;T extends object&gt;(mock: T): PgDatabase =&gt;
  mock as unknown as PgDatabase;

let logger: Logger;

// テスト前の共通セットアップ
beforeEach(() =&gt; {
  logger = createMockLogger();
});

describe("CreateUserUseCase", () =&gt; {
  it("ユーザーを作成できること", async () =&gt; {
    // DBのモック化
    const user = {
      uid: "00000000-0000-0000-0000-000000000001",
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    };
    const returning = vi.fn().mockResolvedValue([user]);
    const values = vi.fn().mockReturnValue({ returning });
    const insert = vi.fn().mockReturnValue({ values });
    const db = createMockDb({
      insert,
    });

    // ユースケース作成
    const useCase = new CreateUserUseCase(db);

    // ユースケース実行
    const result = await useCase.execute(logger, {
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    });

    // 検証
    expect(result).toEqual(user);

    expect(logger.info).toHaveBeenCalledWith("Create user", {
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    });

    expect(values).toHaveBeenCalledWith({
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    });
  });
});

describe("FindUsersUseCase", () =&gt; {
  it("全てのユーザーを取得できること", async () =&gt; {
    // DBのモック化
    const users = [
      {
        uid: "00000000-0000-0000-0000-000000000001",
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: 1000,
      },
      {
        uid: "00000000-0000-0000-0000-000000000002",
        lastName: "佐藤",
        firstName: "花子",
        totalPurchaseAmount: 2000,
      },
    ];
    const from = vi.fn().mockResolvedValue(users);
    const select = vi.fn().mockReturnValue({ from });
    const db = createMockDb({
      select,
    });

    // ユースケース作成
    const useCase = new FindUsersUseCase(db);

    // ユースケース実行
    const result = await useCase.execute(logger);

    // 検証
    expect(result).toEqual(users);

    expect(logger.info).toHaveBeenCalledWith("Find users");
  });
});

describe("FindUserUseCase", () =&gt; {
  it("指定したユーザーを取得できること", async () =&gt; {
    // DBのモック化
    const user = {
      uid: "00000000-0000-0000-0000-000000000001",
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    };
    const limit = vi.fn().mockResolvedValue([user]);
    const where = vi.fn().mockReturnValue({ limit });
    const from = vi.fn().mockReturnValue({ where });
    const select = vi.fn().mockReturnValue({ from });
    const db = createMockDb({
      select,
    });

    // ユースケース作成
    const useCase = new FindUserUseCase(db);

    // ユースケース実行
    const result = await useCase.execute(logger, {
      uid: user.uid,
    });

    // 検証
    expect(result).toEqual(user);

    expect(logger.info).toHaveBeenCalledWith("Find user", {
      uid: user.uid,
    });

    expect(limit).toHaveBeenCalledWith(1);
  });

  it("ユーザーが存在しない場合は空オブジェクトを返すこと", async () =&gt; {
    // DBのモック化
    const limit = vi.fn().mockResolvedValue([]);
    const where = vi.fn().mockReturnValue({ limit });
    const from = vi.fn().mockReturnValue({ where });
    const select = vi.fn().mockReturnValue({ from });
    const db = createMockDb({
      select,
    });

    // ユースケース作成
    const useCase = new FindUserUseCase(db);

    // ユースケース実行
    const result = await useCase.execute(logger, {
      uid: "00000000-0000-0000-0000-000000000099",
    });

    // 検証
    expect(result).toEqual({});

    expect(logger.info).toHaveBeenCalledWith("Find user", {
      uid: "00000000-0000-0000-0000-000000000099",
    });
  });
});

describe("UpdateUserUseCase", () =&gt; {
  it("ユーザーを更新できること", async () =&gt; {
    // DBのモック化
    const user = {
      uid: "00000000-0000-0000-0000-000000000001",
      lastName: "山田",
      firstName: "次郎",
      totalPurchaseAmount: 3000,
    };
    const returning = vi.fn().mockResolvedValue([user]);
    const where = vi.fn().mockReturnValue({ returning });
    const set = vi.fn().mockReturnValue({ where });
    const update = vi.fn().mockReturnValue({ set });
    const transaction = vi.fn(async (callback) =&gt; {
      return callback({
        update,
      });
    });
    const db = createMockDb({
      transaction,
    });

    // ユースケース作成
    const useCase = new UpdateUserUseCase(db);

    // ユースケース実行
    const result = await useCase.execute(logger, {
      uid: user.uid,
      lastName: "山田",
      firstName: "次郎",
      totalPurchaseAmount: 3000,
    });

    // 検証
    expect(result).toEqual(user);

    expect(logger.info).toHaveBeenCalledWith("Update user", {
      uid: user.uid,
      lastName: "山田",
      firstName: "次郎",
      totalPurchaseAmount: 3000,
    });

    expect(set).toHaveBeenCalledWith({
      lastName: "山田",
      firstName: "次郎",
      totalPurchaseAmount: 3000,
      updatedAt: expect.any(Date),
    });
  });

  it("ユーザーが存在しない場合はUserNotFoundErrorを投げること", async () =&gt; {
    // DBのモック化
    const returning = vi.fn().mockResolvedValue([]);
    const where = vi.fn().mockReturnValue({ returning });
    const set = vi.fn().mockReturnValue({ where });
    const update = vi.fn().mockReturnValue({ set });
    const transaction = vi.fn(async (callback) =&gt; {
      return callback({
        update,
      });
    });
    const db = createMockDb({
      transaction,
    });

    // ユースケース作成
    const useCase = new UpdateUserUseCase(db);

    // ユースケース実行・検証
    await expect(
      useCase.execute(logger, {
        uid: "00000000-0000-0000-0000-000000000099",
        lastName: "山田",
        firstName: "次郎",
        totalPurchaseAmount: 3000,
      }),
    ).rejects.toBeInstanceOf(UserNotFoundError);

    expect(logger.info).toHaveBeenCalledWith("Update user", {
      uid: "00000000-0000-0000-0000-000000000099",
      lastName: "山田",
      firstName: "次郎",
      totalPurchaseAmount: 3000,
    });
  });
});

describe("DeleteUserUseCase", () =&gt; {
  it("ユーザーを削除できること", async () =&gt; {
    // DBのモック化
    const user = {
      uid: "00000000-0000-0000-0000-000000000001",
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    };
    const returning = vi.fn().mockResolvedValue([user]);
    const where = vi.fn().mockReturnValue({ returning });
    const deleteUser = vi.fn().mockReturnValue({ where });
    const db = createMockDb({
      delete: deleteUser,
    });

    // ユースケース作成
    const useCase = new DeleteUserUseCase(db);

    // ユースケース実行
    const result = await useCase.execute(logger, {
      uid: user.uid,
    });

    // 検証
    expect(result).toEqual(user);

    expect(logger.info).toHaveBeenCalledWith("Delete user", {
      uid: user.uid,
    });
  });

  it("ユーザーが存在しない場合はUserNotFoundErrorを投げること", async () =&gt; {
    // DBのモック化
    const returning = vi.fn().mockResolvedValue([]);
    const where = vi.fn().mockReturnValue({ returning });
    const deleteUser = vi.fn().mockReturnValue({ where });
    const db = createMockDb({
      delete: deleteUser,
    });

    // ユースケース作成
    const useCase = new DeleteUserUseCase(db);

    // ユースケース実行・検証
    await expect(
      useCase.execute(logger, {
        uid: "00000000-0000-0000-0000-000000000099",
      }),
    ).rejects.toBeInstanceOf(UserNotFoundError);

    expect(logger.info).toHaveBeenCalledWith("Delete user", {
      uid: "00000000-0000-0000-0000-000000000099",
    });
  });
});</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/presentation/http/generic/user/handler.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/generic/user/handler.ts"><code>import type { Context } from "hono";
import type {
  CreateUserInput,
  CreateUserUseCase,
  DeleteUserInput,
  DeleteUserUseCase,
  FindUserInput,
  FindUsersUseCase,
  FindUserUseCase,
  UpdateUserInput,
  UpdateUserUseCase,
} from "../../../../generic/user/useCase.js";

export const createCreateUserHandler =
  (useCase: CreateUserUseCase) =&gt;
  async (c: Context, input: CreateUserInput) =&gt; {
    // 共通ロガー取得
    const logger = c.get("logger");

    // ユースケース実行
    const result = await useCase.execute(logger, input);

    return c.json(result, 201);
  };

export const createFindUsersHandler =
  (useCase: FindUsersUseCase) =&gt; async (c: Context) =&gt; {
    // 共通ロガー取得
    const logger = c.get("logger");

    // ユースケース実行
    const result = await useCase.execute(logger);

    return c.json(result);
  };

export const createFindUserHandler =
  (useCase: FindUserUseCase) =&gt; async (c: Context, input: FindUserInput) =&gt; {
    // 共通ロガー取得
    const logger = c.get("logger");

    // ユースケース実行
    const result = await useCase.execute(logger, input);

    return c.json(result);
  };

export const createUpdateUserHandler =
  (useCase: UpdateUserUseCase) =&gt;
  async (c: Context, input: UpdateUserInput) =&gt; {
    // 共通ロガー取得
    const logger = c.get("logger");

    // ユースケース実行
    const result = await useCase.execute(logger, input);

    return c.json(result);
  };

export const createDeleteUserHandler =
  (useCase: DeleteUserUseCase) =&gt;
  async (c: Context, input: DeleteUserInput) =&gt; {
    // 共通ロガー取得
    const logger = c.get("logger");

    // ユースケース実行
    const _result = await useCase.execute(logger, input);

    return c.body(null, 204);
  };</code></pre>
</div>
<p><span style="color: #ff0000;">※関数を2段階で実行するため、2回連続で「=&gt;」を使っている。</span></p>
<p>&nbsp;</p>
<p>・「src/presentation/http/generic/user/routes.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/generic/user/routes.ts"><code>import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
import type {
  CreateUserInput,
  CreateUserUseCase,
  DeleteUserInput,
  DeleteUserUseCase,
  FindUserInput,
  FindUsersUseCase,
  FindUserUseCase,
  UpdateUserInput,
  UpdateUserUseCase,
} from "../../../../generic/user/useCase.js";
import {
  createCreateUserHandler,
  createDeleteUserHandler,
  createFindUserHandler,
  createFindUsersHandler,
  createUpdateUserHandler,
} from "./handler.js";

// ユーザー作成用のリクエストボディスキーマ定義
const createUserRequestBodySchema = z.object({
  lastName: z
    .string()
    .min(1, "lastNameは必須です")
    .max(10, "lastNameは10文字以内で入力してください"),
  firstName: z
    .string()
    .min(1, "firstNameは必須です")
    .max(10, "firstNameは10文字以内で入力してください"),
  totalPurchaseAmount: z
    .number()
    .min(0, "totalPurchaseAmountは0以上の数値で入力してください"),
});

// ユーザー取得用のリクエストパラメータのスキーマ定義
const findUserRequestParamSchema = z.object({
  uid: z.uuid().min(1, "uidは必須です"),
});

// ユーザー更新用のリクエストパラメータのスキーマ定義
const updateUserRequestParamSchema = z.object({
  uid: z.uuid().min(1, "uidは必須です"),
});

// ユーザー更新用のリクエストボディスキーマ定義
const updateUserRequestBodySchema = z.object({
  lastName: z
    .string()
    .min(1, "lastNameは必須です")
    .max(10, "lastNameは10文字以内で入力してください"),
  firstName: z
    .string()
    .min(1, "firstNameは必須です")
    .max(10, "firstNameは10文字以内で入力してください"),
  totalPurchaseAmount: z
    .number()
    .min(0, "totalPurchaseAmountは0以上の数値で入力してください"),
});

// ユーザー削除用のリクエストパラメータのスキーマ定義
const deleteUserRequestParamSchema = z.object({
  uid: z.uuid().min(1, "uidは必須です"),
});

export const createUserRoutes = (
  createUserUseCase: CreateUserUseCase,
  findUsersUseCase: FindUsersUseCase,
  findUserUseCase: FindUserUseCase,
  updateUserUseCase: UpdateUserUseCase,
  deleteUserUseCase: DeleteUserUseCase,
) =&gt; {
  // Honoでルーティングを作る際は、メソッドチェーン推薦
  const routes = new Hono()
    .post("/", zValidator("json", createUserRequestBodySchema), (c) =&gt; {
      // バリデーション済みのリクエストパラメータを取得
      const { lastName, firstName, totalPurchaseAmount } = c.req.valid("json");

      // インプットパラメータ作成
      const input: CreateUserInput = {
        lastName,
        firstName,
        totalPurchaseAmount,
      };

      return createCreateUserHandler(createUserUseCase)(c, input);
    })
    .get("/", createFindUsersHandler(findUsersUseCase))
    .get("/:uid", zValidator("param", findUserRequestParamSchema), (c) =&gt; {
      // バリデーション済みのリクエストパラメータを取得
      const { uid } = c.req.valid("param");

      // インプットパラメータ作成
      const input: FindUserInput = { uid };

      return createFindUserHandler(findUserUseCase)(c, input);
    })
    .put(
      "/:uid",
      zValidator("param", updateUserRequestParamSchema),
      zValidator("json", updateUserRequestBodySchema),
      (c) =&gt; {
        // バリデーション済みのリクエストパラメータを取得
        const { uid } = c.req.valid("param");
        const { lastName, firstName, totalPurchaseAmount } =
          c.req.valid("json");

        // インプットパラメータ作成
        const input: UpdateUserInput = {
          uid,
          lastName,
          firstName,
          totalPurchaseAmount,
        };

        return createUpdateUserHandler(updateUserUseCase)(c, input);
      },
    )
    .delete("/:uid", zValidator("param", deleteUserRequestParamSchema), (c) =&gt; {
      // バリデーション済みのリクエストパラメータを取得
      const { uid } = c.req.valid("param");

      // インプットパラメータ作成
      const input: DeleteUserInput = { uid };

      return createDeleteUserHandler(deleteUserUseCase)(c, input);
    });

  return routes;
};</code></pre>
</div>
<p><span style="color: #ff0000;">※ルーティングの方でバリデーションチェックを行い、チェック済みのリクエストパラメータをユースケースへ渡して使用する。また、Honoでルーティングを作る際は、メソッドチェーン推薦。</span></p>
<p>&nbsp;</p>
<p>・「src/presentation/http/generic/user/routes.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/generic/user/routes.test.ts"><code>import { Hono } from "hono";
import { testClient } from "hono/testing";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createApp } from "../../../../app.js";
import type {
  CreateUserUseCase,
  DeleteUserUseCase,
  FindUsersUseCase,
  FindUserUseCase,
  UpdateUserUseCase,
} from "../../../../generic/user/useCase.js";
import type { NodeConfig } from "../../../../runtime/node/config.js";
import { config } from "../../../../runtime/node/config.js";
import { createUserRoutes } from "./routes.js";

// ログ出力の無効化
vi.spyOn(console, "log").mockImplementation(() =&gt; {});

// UseCaseのモック
type MockUseCase = {
  execute: ReturnType&lt;typeof vi.fn&gt;;
};
let createUserUseCase: MockUseCase;
let findUsersUseCase: MockUseCase;
let findUserUseCase: MockUseCase;
let updateUserUseCase: MockUseCase;
let deleteUserUseCase: MockUseCase;

// テスト用クライアント作成関数
const createTestClient = (config: NodeConfig) =&gt; {
  // ルーティング設定
  const v1 = new Hono().route(
    "/users",
    createUserRoutes(
      createUserUseCase as unknown as CreateUserUseCase,
      findUsersUseCase as unknown as FindUsersUseCase,
      findUserUseCase as unknown as FindUserUseCase,
      updateUserUseCase as unknown as UpdateUserUseCase,
      deleteUserUseCase as unknown as DeleteUserUseCase,
    ),
  );
  const routes = new Hono().route("/v1", v1);
  const app = createApp(config).route("/api", routes);

  return testClient(app);
};

// テスト用クライアント変数
let client: ReturnType&lt;typeof createTestClient&gt;;

// テスト用コンフィグ変数
let testConfig: NodeConfig;

// テストデータ用のUID
let uid: string;

// テスト前の共通セットアップ
beforeEach(() =&gt; {
  // モックの初期化
  createUserUseCase = {
    execute: vi.fn().mockResolvedValue({
      uid: "550e8400-e29b-41d4-a716-446655440000",
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    }),
  };

  findUsersUseCase = {
    execute: vi.fn().mockResolvedValue([]),
  };

  findUserUseCase = {
    execute: vi.fn().mockResolvedValue({
      uid: "550e8400-e29b-41d4-a716-446655440000",
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    }),
  };

  updateUserUseCase = {
    execute: vi.fn().mockResolvedValue({
      uid: "550e8400-e29b-41d4-a716-446655440000",
      lastName: "山田",
      firstName: "次郎",
      totalPurchaseAmount: 2000,
    }),
  };

  deleteUserUseCase = {
    execute: vi.fn().mockResolvedValue({
      uid: "550e8400-e29b-41d4-a716-446655440000",
    }),
  };

  testConfig = {
    ...config,
  };

  client = createTestClient(testConfig);

  uid = "550e8400-e29b-41d4-a716-446655440000";
});

describe("POST /api/v1/users", () =&gt; {
  it("正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(201);
    expect(createUserUseCase.execute).toHaveBeenCalledTimes(1);
    expect(createUserUseCase.execute).toHaveBeenCalledWith(expect.anything(), {
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
    });
  });

  it("lastNameが未指定の場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        firstName: "太郎",
        totalPurchaseAmount: 1000,
      } as never,
    });

    // 検証
    expect(response.status).toBe(400);
    expect(createUserUseCase.execute).not.toHaveBeenCalled();
  });

   it("firstNameが未指定の場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        totalPurchaseAmount: 1000,
      } as never,
    });

    // 検証
    expect(response.status).toBe(400);
    expect(createUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("totalPurchaseAmountが未指定の場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        firstName: "太郎",
      } as never,
    });

    // 検証
    expect(response.status).toBe(400);
    expect(createUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("lastNameが10文字以内の場合に正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "あいうえおかきくけこ",
        firstName: "太郎",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(201);
    expect(createUserUseCase.execute).toHaveBeenCalledTimes(1);
  });

  it("lastNameが10文字を超えている場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "あいうえおかきくけこさ",
        firstName: "太郎",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(createUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("firstNameが10文字以内の場合に正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        firstName: "あいうえおかきくけこ",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(201);
    expect(createUserUseCase.execute).toHaveBeenCalledTimes(1);
  });

  it("firstNameが10文字を超えている場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        firstName: "あいうえおかきくけこさ",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(createUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("totalPurchaseAmountが0の場合に正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: 0,
      },
    });

    // 検証
    expect(response.status).toBe(201);
    expect(createUserUseCase.execute).toHaveBeenCalledTimes(1);
  });

  it("totalPurchaseAmountがマイナスの場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: -1,
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(createUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("totalPurchaseAmountが文字列の場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: "1000",
      } as never,
    });

    // 検証
    expect(response.status).toBe(400);
    expect(createUserUseCase.execute).not.toHaveBeenCalled();
  });
});

describe("GET /api/v1/users", () =&gt; {
  it("正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$get();

    // 検証
    expect(response.status).toBe(200);
    expect(findUsersUseCase.execute).toHaveBeenCalledTimes(1);
  });
});

describe("GET /api/v1/users/:uid", () =&gt; {
  it("正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$get({
      param: {
        uid: uid,
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(findUserUseCase.execute).toHaveBeenCalledTimes(1);
    expect(findUserUseCase.execute).toHaveBeenCalledWith(expect.anything(), {
      uid,
    });
  });

  it("uidがUUID形式ではない場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$get({
      param: {
        uid: "invalid-uid",
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(findUserUseCase.execute).not.toHaveBeenCalled();
  });
});

describe("PUT /api/v1/users/:uid", () =&gt; {
  it("正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "山田",
        firstName: "次郎",
        totalPurchaseAmount: 2000,
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(updateUserUseCase.execute).toHaveBeenCalledTimes(1);
    expect(updateUserUseCase.execute).toHaveBeenCalledWith(expect.anything(), {
      uid,
      lastName: "山田",
      firstName: "次郎",
      totalPurchaseAmount: 2000,
    });
  });

  it("uidがUUID形式ではない場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: "invalid-uid",
      },
      json: {
        lastName: "山田",
        firstName: "次郎",
        totalPurchaseAmount: 2000,
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(updateUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("lastNameが未指定の場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        firstName: "次郎",
        totalPurchaseAmount: 2000,
      } as never,
    });

    // 検証
    expect(response.status).toBe(400);
    expect(updateUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("firstNameが未指定の場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "山田",
        totalPurchaseAmount: 2000,
      } as never,
    });

    // 検証
    expect(response.status).toBe(400);
    expect(updateUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("totalPurchaseAmountが未指定の場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "山田",
        firstName: "次郎",
      } as never,
    });

    // 検証
    expect(response.status).toBe(400);
    expect(updateUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("lastNameが10文字以内の場合に正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "あいうえおかきくけこ",
        firstName: "次郎",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(updateUserUseCase.execute).toHaveBeenCalledTimes(1);
  });

  it("lastNameが10文字を超えている場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "あいうえおかきくけこさ",
        firstName: "次郎",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(updateUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("firstNameが10文字以内の場合に正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "山田",
        firstName: "あいうえおかきくけこ",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(updateUserUseCase.execute).toHaveBeenCalledTimes(1);
  });

  it("firstNameが10文字を超えている場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "山田",
        firstName: "あいうえおかきくけこさ",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(updateUserUseCase.execute).not.toHaveBeenCalled();
  });

  it("totalPurchaseAmountが0の場合に正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: 0,
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(updateUserUseCase.execute).toHaveBeenCalledTimes(1);
  });

  it("totalPurchaseAmountがマイナスの場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: uid,
      },
      json: {
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: -1,
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(updateUserUseCase.execute).not.toHaveBeenCalled();
  });
});

describe("DELETE /api/v1/users/:uid", () =&gt; {
  it("正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$delete({
      param: {
        uid: uid,
      },
    });

    // 検証
    expect(response.status).toBe(204);
    expect(deleteUserUseCase.execute).toHaveBeenCalledTimes(1);
    expect(deleteUserUseCase.execute).toHaveBeenCalledWith(expect.anything(), {
      uid,
    });
  });

  it("uidがUUID形式ではない場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users[":uid"].$delete({
      param: {
        uid: "invalid-uid",
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(deleteUserUseCase.execute).not.toHaveBeenCalled();
  });
});</code></pre>
</div>
<p><span style="color: #ff0000;">※エラー系では「as never」を使う</span></p>
<p>&nbsp;</p>
<p>・「src/tests/integration/generic/user/user.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/tests/integration/generic/user/user.test.ts"><code>import { eq } from "drizzle-orm";
import { testClient } from "hono/testing";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { DbPg } from "../../../../infrastructure/database/drizzle/pg/client.js";
import { createDbPg } from "../../../../infrastructure/database/drizzle/pg/client.js";
import { users } from "../../../../infrastructure/database/drizzle/pg/schema/users.js";
import { createNodeApp } from "../../../../runtime/node/app.js";
import type { NodeConfig } from "../../../../runtime/node/config.js";
import { config } from "../../../../runtime/node/config.js";

// ログ出力の無効化
vi.spyOn(console, "log").mockImplementation(() =&gt; {});

// テスト用クライアント作成関数
const createTestClient = (config: NodeConfig) =&gt; {
  return testClient(createNodeApp(config));
};

// テスト用クライアント変数
let client: ReturnType&lt;typeof createTestClient&gt;;

let testDbPg: DbPg;

// テスト用コンフィグ変数
let testConfig: NodeConfig;

// テスト前の共通セットアップ
beforeEach(() =&gt; {
  testConfig = {
    ...config,
    db: {
      ...config.db,
      name: "testing-db-pg",
    },
  };

  client = createTestClient(testConfig);
  testDbPg = createDbPg(testConfig.db);
});

// テスト後の共通クリーンアップ
afterEach(async () =&gt; {
  await testDbPg.db.delete(users);
  await testDbPg.pool.end();
});

describe("POST /api/v1/users", () =&gt; {
  it("ユーザーが新規作成され、正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.users.$post({
      json: {
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: 1000,
      },
    });

    // 検証
    expect(response.status).toBe(201);

    // レスポンス結果の検証
    type CreateUserResponse = {
      id: number;
      uid: string;
      lastName: string;
      firstName: string;
      totalPurchaseAmount: number;
      createdAt: string;
      updatedAt: string;
    };
    const body = (await response.json()) as CreateUserResponse;
    expect(body).toEqual({
      id: expect.any(Number),
      uid: expect.any(String),
      lastName: "山田",
      firstName: "太郎",
      totalPurchaseAmount: 1000,
      createdAt: expect.any(String),
      updatedAt: expect.any(String),
    });

    // DBに保存されていることを検証
    const [user] = await testDbPg.db
      .select()
      .from(users)
      .where(eq(users.uid, body.uid))
      .limit(1);

    expect(user).toEqual({
      id: body.id,
      uid: body.uid,
      lastName: body.lastName,
      firstName: body.firstName,
      totalPurchaseAmount: body.totalPurchaseAmount,
      createdAt: new Date(body.createdAt),
      updatedAt: new Date(body.updatedAt),
    });
  });
});

describe("GET /api/v1/users", () =&gt; {
  it("全てのユーザーを取得し、正常終了すること", async () =&gt; {
    // テストデータ登録
    const resultUsers = await testDbPg.db
      .insert(users)
      .values([
        {
          lastName: "山田",
          firstName: "太郎",
          totalPurchaseAmount: 1000,
        },
        {
          lastName: "佐藤",
          firstName: "花子",
          totalPurchaseAmount: 2000,
        },
      ])
      .returning();
    const expectedUsers = resultUsers.map((user) =&gt; ({
      ...user,
      createdAt: user.createdAt.toISOString(),
      updatedAt: user.updatedAt.toISOString(),
    }));

    // テスト実行
    const response = await client.api.v1.users.$get();

    // 検証
    expect(response.status).toBe(200);

    // レスポンス結果の検証
    const body = await response.json();
    expect(body).toHaveLength(2);
    expect(body).toEqual(expectedUsers);
  });
});

describe("GET /api/v1/users/:uid", () =&gt; {
  it("対象ユーザーを取得し、正常終了すること", async () =&gt; {
    // テストデータ登録
    const [resultUser] = await testDbPg.db
      .insert(users)
      .values({
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: 1000,
    })
    .returning();
    const expectedUser = {
      ...resultUser,
      createdAt: resultUser.createdAt.toISOString(),
      updatedAt: resultUser.updatedAt.toISOString(),
    };

    // テスト実行
    const response = await client.api.v1.users.$get();

    // 検証
    expect(response.status).toBe(200);

    // レスポンス結果の検証
    const [body] = await response.json();
    expect(body).toEqual(expectedUser);
  });
});

describe("PUT /api/v1/users/:uid", () =&gt; {
  it("対象ユーザーを更新し、正常終了すること", async () =&gt; {
    // テストデータ登録
    const [resultUser] = await testDbPg.db
      .insert(users)
      .values({
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: 1000,
      })
      .returning();
    const expectedUser = {
      ...resultUser,
      lastName: "佐藤",
      firstName: "二郎",
      totalPurchaseAmount: 2000,
      createdAt: resultUser.createdAt.toISOString(),
      updatedAt: expect.any(String),
    };

    // テスト実行
    const response = await client.api.v1.users[":uid"].$put({
      param: {
        uid: expectedUser.uid,
      },
      json: {
        lastName: expectedUser.lastName,
        firstName: expectedUser.firstName,
        totalPurchaseAmount: expectedUser.totalPurchaseAmount,
      },
    });

    // 検証
    expect(response.status).toBe(200);

    // レスポンス結果の検証
    const body = await response.json();
    expect(body).toEqual(expectedUser);
    if ("createdAt" in body) {
      expect(body.createdAt).toBe(expectedUser.createdAt);
    }
    if ("updatedAt" in body) {
      expect(body.updatedAt).not.toBe(expectedUser.createdAt);
    }
  });
});

describe("DELETE /api/v1/users/:uid", () =&gt; {
  it("対象ユーザーを削除し、正常終了すること", async () =&gt; {
    // テストデータ登録
    const [resultUser] = await testDbPg.db
      .insert(users)
      .values({
        lastName: "山田",
        firstName: "太郎",
        totalPurchaseAmount: 1000,
      })
      .returning();

    // テスト実行
    const response = await client.api.v1.users[":uid"].$delete({
      param: {
        uid: resultUser.uid,
      },
    });

    // 検証
    expect(response.status).toBe(204);

    // DBから削除されていることを確認
    const deletedUsers = await testDbPg.db
      .select()
      .from(users)
      .where(eq(users.uid, resultUser.uid));

    expect(deletedUsers).toHaveLength(0);
  });
});</code></pre>
</div>
<p><span style="color: #ff0000;">※インテグレーションテスト</span></p>
<p>&nbsp;</p>
<p>次にファイル「src/di/nodeContainer.ts」、「src/presentation/http/routesNode.ts」をそれぞれ以下のように修正します。</p>
<p>・「src/di/nodeContainer.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/di/nodeContainer.ts"><code>import {
  CreateUserUseCase,
  DeleteUserUseCase,
  FindUsersUseCase,
  FindUserUseCase,
  UpdateUserUseCase,
} from "../generic/user/useCase.js";
import { createDbPg } from "../infrastructure/database/drizzle/pg/client.js";
import type { NodeConfig } from "../runtime/node/config.js";
import {
  SampleHelloUseCase,
  SampleTextUseCase,
} from "../supporting/sample/useCase.js";

export const createNodeContainer = (config: NodeConfig) =&gt; {
  // DBインスタンスの初期化
  const dbPg = createDbPg(config.db);

  // サンプル関連のユースケースを初期化
  const sampleHelloUseCase = new SampleHelloUseCase();
  const sampleTextUseCase = new SampleTextUseCase();

  // ユーザー関連のユースケースを初期化
  const createUserUseCase = new CreateUserUseCase(dbPg.db);
  const findUsersUseCase = new FindUsersUseCase(dbPg.db);
  const findUserUseCase = new FindUserUseCase(dbPg.db);
  const updateUserUseCase = new UpdateUserUseCase(dbPg.db);
  const deleteUserUseCase = new DeleteUserUseCase(dbPg.db);

  return {
    sampleHelloUseCase,
    sampleTextUseCase,
    createUserUseCase,
    findUsersUseCase,
    findUserUseCase,
    updateUserUseCase,
    deleteUserUseCase,
  };
};

export type NodeContainer = ReturnType&lt;typeof createNodeContainer&gt;;</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/presentation/http/routesNode.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/routesNode.ts"><code>import { Hono } from "hono";
import type { NodeContainer } from "../../di/nodeContainer.js";
import { createUserRoutes } from "../http/generic/user/routes.js";
import { createSampleRoutes } from "../http/supporting/sample/routes.js";

export const createRoutesNode = (container: NodeContainer) =&gt; {
  /******************************
  * v1用のルーティング
  ******************************/
  // Honoでルーティングを作る際は、メソッドチェーン推薦
  const v1 = new Hono()
    .route(
      "/sample",
      createSampleRoutes(
        container.sampleHelloUseCase,
        container.sampleTextUseCase,
      ),
    )
    .route(
      "/users",
      createUserRoutes(
        container.createUserUseCase,
        container.findUsersUseCase,
        container.findUserUseCase,
        container.updateUserUseCase,
        container.deleteUserUseCase,
      ),
    );
  /******************************
  * ルーティング設定
  ******************************/
  return new Hono().route("/v1", v1);
};</code></pre>
</div>
<p><span style="color: #ff0000;">※Honoでルーティングを作る際は、メソッドチェーン推薦。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、各種コードチェックします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm run format
$ pnpm run lint
$ pnpm run test</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のように全てのテストがパスすればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-30.png" alt="" width="1002" height="440" class="aligncenter size-full wp-image-20701" />
<p>&nbsp;</p>
<p>次にPostmanで作成したAPIを試してみます。</p>
<p>まずはPOSTメソッドで「http://localhost:3000/api/v1/users」を実行後、以下のように正常終了し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-23.png" alt="" width="2542" height="1584" class="aligncenter size-full wp-image-20702" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-23.png 2542w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-23-300x187.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-23-1024x638.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-23-768x479.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-23-1536x957.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-23-2048x1276.png 2048w" sizes="auto, (max-width: 2542px) 100vw, 2542px" />
<p>&nbsp;</p>
<p>次にGETメソッドで「http://localhost:3000/api/v1/users」を実行後、以下のように正常終了し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-24.png" alt="" width="2540" height="1582" class="aligncenter size-full wp-image-20703" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-24.png 2540w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-24-300x187.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-24-1024x638.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-24-768x478.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-24-1536x957.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-24-2048x1276.png 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次にGETメソッドで「http://localhost:3000/api/v1/users/:uid」を実行後、以下のように正常終了し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-25.png" alt="" width="2544" height="1578" class="aligncenter size-full wp-image-20704" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-25.png 2544w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-25-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-25-1024x635.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-25-768x476.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-25-1536x953.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-25-2048x1270.png 2048w" sizes="auto, (max-width: 2544px) 100vw, 2544px" />
<p>&nbsp;</p>
<p>次にPUTメソッドで「http://localhost:3000/api/v1/users/:uid」を実行後、以下のように正常終了し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-26.png" alt="" width="2544" height="1574" class="aligncenter size-full wp-image-20705" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-26.png 2544w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-26-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-26-1024x634.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-26-768x475.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-26-1536x950.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-26-2048x1267.png 2048w" sizes="auto, (max-width: 2544px) 100vw, 2544px" />
<p>&nbsp;</p>
<p>次に再度GETメソッドで「http://localhost:3000/api/v1/users/:uid」を実行後、以下のように正常終了し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-27.png" alt="" width="2540" height="1576" class="aligncenter size-full wp-image-20706" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-27.png 2540w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-27-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-27-1024x635.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-27-768x477.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-27-1536x953.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-27-2048x1271.png 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次にDELETEメソッドで「http://localhost:3000/api/v1/users/:uid」を実行後、以下のように正常終了し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-28.png" alt="" width="2540" height="1578" class="aligncenter size-full wp-image-20707" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-28.png 2540w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-28-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-28-1024x636.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-28-768x477.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-28-1536x954.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-28-2048x1272.png 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次にGETメソッドで「http://localhost:3000/api/v1/users」を実行後、以下のように正常終了し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-29.png" alt="" width="2536" height="1572" class="aligncenter size-full wp-image-20708" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-29.png 2536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-29-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-29-1024x635.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-29-768x476.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-29-1536x952.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-29-2048x1270.png 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>合計購入金額から会員ランクを出力するAPIを作成する</h2>
<p>次は上記で作成したusersテーブルを利用し、memberドメインとして合計購入金額から会員ランクを出力するAPIを作成してみます。</p>
<p>&nbsp;</p>
<h3>Memberドメインを作る</h3>
<p>まずはMemberドメインを作るため、以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/core/member/domain
$ touch src/core/member/domain/memberUid.ts src/core/member/domain/memberUid.test.ts
$ touch src/core/member/domain/member.ts src/core/member/domain/member.test.ts
$ touch src/core/member/domain/memberRankService.ts src/core/member/domain/memberRankService.test.ts
$ touch src/core/member/domain/memberRepository.ts
$ mkdir -p src/core/member/infrastructure/persistence/pg/query
$ touch src/core/member/infrastructure/persistence/pg/query/drizzleMemberRepository.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/core/member/domain/memberUid.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/domain/memberUid.ts"><code>export class MemberUid {
  constructor(readonly value: string) {
    if (!MemberUid.isValid(value)) {
      throw new Error(`Invalid MemberUid: ${value}`);
    }
  }

  // uuid形式チェック
  private static isValid(value: string): boolean {
    return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
      value,
    );
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※値オブジェクト</span></p>
<p>&nbsp;</p>
<p>・「src/core/member/domain/memberUid.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/domain/memberUid.test.ts"><code>import { describe, expect, it } from "vitest";
import { MemberUid } from "./memberUid.js";

describe("MemberUid", () =&gt; {
  it("有効なUUIDをMemberUidとして生成できる", () =&gt; {
    const value = "550e8400-e29b-41d4-a716-446655440000";

    const memberUid = new MemberUid(value);

    expect(memberUid.value).toBe(value);
  });

  it("不正なUUIDの場合はエラーになる", () =&gt; {
    expect(() =&gt; {
      new MemberUid("invalid-uuid");
    }).toThrow("Invalid MemberUid: invalid-uuid");
  });

  it("空文字の場合はエラーになる", () =&gt; {
    expect(() =&gt; {
      new MemberUid("");
    }).toThrow("Invalid MemberUid: ");
  });
});</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/core/member/domain/member.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/domain/member.ts"><code>import type { MemberUid } from "./memberUid.js";

export class Member {
  constructor(
    readonly uid: MemberUid,
    readonly lastName: string,
    readonly firstName: string,
    readonly totalPurchaseAmount: number,
  ) {}

  // フルネームを出力するメソッド
  get fullName(): string {
    return `${this.lastName} ${this.firstName}`;
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※ドメイン</span></p>
<p>&nbsp;</p>
<p>・「src/core/member/domain/member.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/domain/member.test.ts"><code>import { describe, expect, it } from "vitest";
import { Member } from "./member.js";
import { MemberUid } from "./memberUid.js";

describe("Member", () =&gt; {
  it("メンバー情報を保持する", () =&gt; {
    const uid = new MemberUid("550e8400-e29b-41d4-a716-446655440000");

    const member = new Member(uid, "山田", "太郎", 10000);

    expect(member.uid).toBe(uid);
    expect(member.lastName).toBe("山田");
    expect(member.firstName).toBe("太郎");
    expect(member.totalPurchaseAmount).toBe(10000);
  });

  it("fullNameに姓と名を結合した名前を返す", () =&gt; {
    const member = new Member(
      new MemberUid("550e8400-e29b-41d4-a716-446655440000"),
      "山田",
      "太郎",
      10000,
    );

    expect(member.fullName).toBe("山田 太郎");
  });
});</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/core/member/domain/memberRankService.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/domain/memberRankService.ts"><code>import type { Member } from "./member.js";

export type MemberRank = "bronze" | "silver" | "gold" | "platinum";

export class MemberRankService {
  calculate(member: Member): MemberRank {
    // 100万円以上でプラチナ
    if (member.totalPurchaseAmount &gt;= 1_000_000) {
      return "platinum";
    }

    // 10万円以上でゴールド
    if (member.totalPurchaseAmount &gt;= 100_000) {
      return "gold";
    }

    // 1万円以上でシルバー
    if (member.totalPurchaseAmount &gt;= 10_000) {
      return "silver";
    }

    return "bronze";
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※ドメインサービス</span></p>
<p>&nbsp;</p>
<p>・「src/core/member/domain/memberRankService.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/domain/memberRankService.test.ts"><code>import { describe, expect, it } from "vitest";
import { Member } from "./member.js";
import { MemberRankService } from "./memberRankService.js";
import { MemberUid } from "./memberUid.js";

describe("MemberRankService", () =&gt; {
  const service = new MemberRankService();

  const createMember = (totalPurchaseAmount: number) =&gt; {
    return new Member(
      new MemberUid("550e8400-e29b-41d4-a716-446655440000"),
      "山田",
      "太郎",
      totalPurchaseAmount,
    );
  };

  it("合計購入金額が10,000円未満ならbronze", () =&gt; {
    const member = createMember(9_999);

    expect(service.calculate(member)).toBe("bronze");
  });

  it("合計購入金額が10,000円以上ならsilver", () =&gt; {
    const member = createMember(10_000);

    expect(service.calculate(member)).toBe("silver");
  });

  it("合計購入金額が100,000円以上ならgold", () =&gt; {
    const member = createMember(100_000);

    expect(service.calculate(member)).toBe("gold");
  });

  it("合計購入金額が1,000,000円以上ならplatinum", () =&gt; {
    const member = createMember(1_000_000);

    expect(service.calculate(member)).toBe("platinum");
  });
});</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/core/member/domain/memberRepository.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/domain/memberRepository.ts"><code>import type { Member } from "./member.js";
import type { MemberUid } from "./memberUid.js";

export interface MemberRepository {
  findByUid(uid: MemberUid): Promise&lt;Member | null&gt;;
}</code></pre>
</div>
<p><span style="color: #ff0000;">※リポジトリのインターフェース</span></p>
<p>&nbsp;</p>
<p>・「src/core/member/infrastructure/persistence/pg/query/drizzleMemberRepository.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/infrastructure/persistence/pg/query/drizzleMemberRepository.ts"><code>import { eq } from "drizzle-orm";
import type { PgDbClient } from "../../../../../../infrastructure/database/drizzle/pg/client.js";
import { users } from "../../../../../../infrastructure/database/drizzle/pg/schema/users.js";
import { Member } from "../../../../domain/member.js";
import type { MemberRepository } from "../../../../domain/memberRepository.js";
import { MemberUid } from "../../../../domain/memberUid.js";

export class DrizzleMemberRepository implements MemberRepository {
  constructor(private readonly db: PgDbClient) {}

  async findByUid(uid: MemberUid): Promise&lt;Member | null&gt; {
    // 対象ユーザー取得
    const [user] = await this.db
      .select()
      .from(users)
      .where(eq(users.uid, uid.value))
      .limit(1);

    // ユーザー取得チェック
    if (!user) {
      return null;
    }

    return new Member(
      new MemberUid(user.uid),
      user.lastName,
      user.firstName,
      user.totalPurchaseAmount,
    );
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※リポジトリの実装</span></p>
<p>&nbsp;</p>
<h3>ドメイン層とDB層を繋ぐ部分を作る</h3>
<p>次にドメイン層がDB層に依存しないようにするため、ドメイン層とDB層を繋ぐ部分を作ります。</p>
<p>まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch src/infrastructure/database/database.ts
$ mkdir -p src/infrastructure/repository
$ touch src/infrastructure/repository/drizzleRepositoryFactory.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/infrastructure/database/database.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/infrastructure/database/database.ts"><code>import type { PgDbClient } from "./drizzle/pg/client.js";

export class Database {
  constructor(private readonly db: PgDbClient) {}

  getClient(): PgDbClient {
    return this.db;
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※データベースクラスを作る</span></p>
<p>&nbsp;</p>
<p>・「src/infrastructure/repository/drizzleRepositoryFactory.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/infrastructure/repository/drizzleRepositoryFactory.ts"><code>import { DrizzleMemberRepository } from "../../core/member/infrastructure/persistence/pg/query/drizzleMemberRepository.js";
import type { PgDbClient } from "../database/drizzle/pg/client.js";

export class DrizzleRepositoryFactory {
  create(db: PgDbClient) {
    return {
      member: new DrizzleMemberRepository(db),
    };
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※リポジトリをインスタンス化するためのクラスを作る</span></p>
<p>&nbsp;</p>
<h3>ユースケース層を作る</h3>
<p>次にユースケース層を作るため、以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/core/member/usecase
$ touch src/core/member/usecase/calculateMemberRank.ts src/core/member/usecase/calculateMemberRank.test.ts
$ touch src/core/member/usecase/errors.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/core/member/usecase/calculateMemberRank.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/usecase/calculateMemberRank.ts"><code>import type { Database } from "../../../infrastructure/database/database.js";
import type { DrizzleRepositoryFactory } from "../../../infrastructure/repository/drizzleRepositoryFactory.js";
import type { Logger } from "../../../shared/logger/logger.js";
import { MemberRankService } from "../domain/memberRankService.js";
import { MemberUid } from "../domain/memberUid.js";
import { MemberNotFoundError } from "./errors.js";

export type CalculateMemberRankUseCaseInput = {
  uid: string;
};

export class CalculateMemberRankUseCase {
  constructor(
    private readonly db: Database,
    private readonly repositoryFactory: DrizzleRepositoryFactory,
  ) {}

  async execute(_logger: Logger, input: CalculateMemberRankUseCaseInput) {
    // リポジトリのインスタンス化
    const repo = this.repositoryFactory.create(this.db.getClient());

    // uidからメンバーを取得
    const member = await repo.member.findByUid(new MemberUid(input.uid));

    // 必須チェック
    if (!member) {
      throw new MemberNotFoundError();
    }

    // メンバーのランク取得
    const rank = new MemberRankService().calculate(member);

    return {
      rank,
    };
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※ユースケース層を作る</span></p>
<p>&nbsp;</p>
<p>・「src/core/member/usecase/calculateMemberRank.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/usecase/calculateMemberRank.test.ts"><code>import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Database } from "../../../infrastructure/database/database.js";
import type { PgDbClient } from "../../../infrastructure/database/drizzle/pg/client.js";
import type { DrizzleRepositoryFactory } from "../../../infrastructure/repository/drizzleRepositoryFactory.js";
import type { Logger } from "../../../shared/logger/logger.js";
import { Member } from "../domain/member.js";
import { MemberUid } from "../domain/memberUid.js";
import { CalculateMemberRankUseCase } from "./calculateMemberRank.js";
import { MemberNotFoundError } from "./errors.js";

// ロガーのモック化
const createMockLogger = (): Logger =&gt; ({
  info: vi.fn(),
  warn: vi.fn(),
  error: vi.fn(),
  debug: vi.fn(),
  withRequestId: vi.fn(),
});

// DBのモック化
const createMockDb = &lt;T extends object&gt;(mock: T): PgDbClient =&gt;
  mock as unknown as PgDbClient;

  let logger: Logger;

  beforeEach(() =&gt; {
    logger = createMockLogger();
  });

  describe("CalculateMemberRankUseCase", () =&gt; {
    it("メンバーのランクを取得できること", async () =&gt; {
      // メンバーのモック
      const uid = "550e8400-e29b-41d4-a716-446655440000";

      const member = new Member(new MemberUid(uid), "山田", "太郎", 100_000);

      // Repositoryのモック化
      const findByUid = vi.fn().mockResolvedValue(member);

      const repository = {
        member: {
          findByUid,
        },
      };

      // DBのモック化
      const dbClient = createMockDb({});

      const getClient = vi.fn().mockReturnValue(dbClient);

      const db = {
        getClient,
      } as unknown as Database;

      // RepositoryFactoryのモック化
      const create = vi.fn().mockReturnValue(repository);

      const repositoryFactory = {
        create,
      } as unknown as DrizzleRepositoryFactory;

      // ユースケース作成
      const useCase = new CalculateMemberRankUseCase(db, repositoryFactory);

      // ユースケース実行
      const result = await useCase.execute(logger, {
        uid,
      });

      // 検証
      expect(result).toEqual({
        rank: "gold",
      });
      expect(findByUid).toHaveBeenCalledOnce();
  });

  it("メンバーが存在しない場合はエラーになること", async () =&gt; {
    // Repositoryのモック化
    const findByUid = vi.fn().mockResolvedValue(null);

    const repository = {
      member: {
        findByUid,
      },
    };

    // DBのモック化
    const dbClient = createMockDb({});

    const getClient = vi.fn().mockReturnValue(dbClient);

    const db = {
      getClient,
    } as unknown as Database;

    // RepositoryFactoryのモック化
    const create = vi.fn().mockReturnValue(repository);

    const repositoryFactory = {
      create,
    } as unknown as DrizzleRepositoryFactory;

    // ユースケース作成
    const useCase = new CalculateMemberRankUseCase(db, repositoryFactory);

    // ユースケース実行・検証
    await expect(
      useCase.execute(logger, {
        uid: "550e8400-e29b-41d4-a716-446655440000",
      }),
    ).rejects.toBeInstanceOf(MemberNotFoundError);
  });
});</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/core/member/usecase/errors.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/core/member/usecase/errors.ts"><code>import type { ContentfulStatusCode } from "hono/utils/http-status";
import { AppError } from "../../../shared/error/appError.js";

// エラーコード（カスタムエラー用）
// 「|」で区切って追加可能
export type MemberErrorCode = "MEMBER_NOT_FOUND";

// エラーコードとHTTPステータスコードのマッピング
export const memberErrorStatusMap: Record&lt;
  MemberErrorCode,
  ContentfulStatusCode
&gt; = {
  MEMBER_NOT_FOUND: 404,
};

export class MemberNotFoundError extends AppError&lt;MemberErrorCode&gt; {
  constructor() {
    super("Member not found", "MEMBER_NOT_FOUND", "warn");
  }
}</code></pre>
</div>
<p><span style="color: #ff0000;">※カスタムエラーを作る</span></p>
<p>&nbsp;</p>
<p>次にファイル「src/di/nodeContainer.ts」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/di/nodeContainer.ts"><code>import { CalculateMemberRankUseCase } from "../core/member/usecase/calculateMemberRank.js";
import {
  CreateUserUseCase,
  DeleteUserUseCase,
  FindUsersUseCase,
  FindUserUseCase,
  UpdateUserUseCase,
} from "../generic/user/useCase.js";
import { Database } from "../infrastructure/database/database.js";
import { createDbPg } from "../infrastructure/database/drizzle/pg/client.js";
import { DrizzleRepositoryFactory } from "../infrastructure/repository/drizzleRepositoryFactory.js";
import type { NodeConfig } from "../runtime/node/config.js";
import {
  SampleHelloUseCase,
  SampleTextUseCase,
} from "../supporting/sample/useCase.js";

export const createNodeContainer = (config: NodeConfig) =&gt; {
  // DBインスタンスの初期化
  const dbPg = createDbPg(config.db);

  // サンプル関連のユースケースを初期化
  const sampleHelloUseCase = new SampleHelloUseCase();
  const sampleTextUseCase = new SampleTextUseCase();

  // ユーザー関連のユースケースを初期化
  const createUserUseCase = new CreateUserUseCase(dbPg.db);
  const findUsersUseCase = new FindUsersUseCase(dbPg.db);
  const findUserUseCase = new FindUserUseCase(dbPg.db);
  const updateUserUseCase = new UpdateUserUseCase(dbPg.db);
  const deleteUserUseCase = new DeleteUserUseCase(dbPg.db);

  // メンバードメイン関連のユースケースを初期化
  const database = new Database(dbPg.db);
  const drizzleRepositoryFactory = new DrizzleRepositoryFactory();
  const calculateMemberRankUseCase = new CalculateMemberRankUseCase(
    database,
    drizzleRepositoryFactory,
  );

  return {
    sampleHelloUseCase,
    sampleTextUseCase,
    createUserUseCase,
    findUsersUseCase,
    findUserUseCase,
    updateUserUseCase,
    deleteUserUseCase,
    calculateMemberRankUseCase,
  };
};

export type NodeContainer = ReturnType&lt;typeof createNodeContainer&gt;;</code></pre>
</div>
<p><span style="color: #ff0000;">※DIコンテナでユースケースのインスタンス化する</span></p>
<p>&nbsp;</p>
<h3>ルーティングを作る</h3>
<p>次にルーティングを作成するため、</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/presentation/http/core/member
$ touch src/presentation/http/core/member/handler.ts
$ touch src/presentation/http/core/member/routes.ts src/presentation/http/core/member/routes.test.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/presentation/http/core/member/handler.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/core/member/handler.ts"><code>import type { Context } from "hono";
import type {
  CalculateMemberRankUseCase,
  CalculateMemberRankUseCaseInput,
} from "../../../../core/member/usecase/calculateMemberRank.js";

export const createCalculateMemberRankHandler =
  (useCase: CalculateMemberRankUseCase) =&gt;
  async (c: Context, input: CalculateMemberRankUseCaseInput) =&gt; {
    // 共通ロガー取得
    const logger = c.get("logger");

    // ユースケース実行
    const result = await useCase.execute(logger, input);

    return c.json(result);
  };</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/presentation/http/core/member/routes.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/core/member/routes.ts"><code>import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
import type {
  CalculateMemberRankUseCase,
  CalculateMemberRankUseCaseInput,
} from "../../../../core/member/usecase/calculateMemberRank.js";
import { createCalculateMemberRankHandler } from "./handler.js";

// リクエストパラメータのスキーマ定義
const calculateMemberRankRequestParamSchema = z.object({
  uid: z.uuid().min(1, "uidは必須です"),
});

export const createMemberRoutes = (
  calculateMemberRankUseCase: CalculateMemberRankUseCase,
) =&gt; {
  const routes = new Hono().get(
    "/:uid/rank",
    zValidator("param", calculateMemberRankRequestParamSchema),
    (c) =&gt; {
      // バリデーション済みのリクエストパラメータを取得
      const { uid } = c.req.valid("param");

      // インプットパラメータ作成
      const input: CalculateMemberRankUseCaseInput = { uid };

      return createCalculateMemberRankHandler(calculateMemberRankUseCase)(
        c,
        input,
      );
    },
  );

  return routes;
};</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/presentation/http/core/member/routes.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/core/member/routes.test.ts"><code>import { Hono } from "hono";
import { testClient } from "hono/testing";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createApp } from "../../../../app.js";
import type { CalculateMemberRankUseCase } from "../../../../core/member/usecase/calculateMemberRank.js";
import type { NodeConfig } from "../../../../runtime/node/config.js";
import { config } from "../../../../runtime/node/config.js";
import { createMemberRoutes } from "./routes.js";

// ログ出力の無効化
vi.spyOn(console, "log").mockImplementation(() =&gt; {});

// UseCaseのモック
type MockUseCase = {
  execute: ReturnType&lt;typeof vi.fn&gt;;
};
let calculateMemberRankUseCase: MockUseCase;

// テスト用クライアント作成関数
const createTestClient = (config: NodeConfig) =&gt; {
  // ルーティング設定
  const v1 = new Hono().route(
    "/members",
    createMemberRoutes(
      calculateMemberRankUseCase as unknown as CalculateMemberRankUseCase,
    ),
  );
  const routes = new Hono().route("/v1", v1);
  const app = createApp(config).route("/api", routes);

  return testClient(app);
};

// テスト用クライアント変数
let client: ReturnType&lt;typeof createTestClient&gt;;

// テスト用コンフィグ変数
let testConfig: NodeConfig;

// テストデータ用のUID
let uid: string;

// テスト前の共通セットアップ
beforeEach(() =&gt; {
  // モックの初期化
  calculateMemberRankUseCase = {
    execute: vi.fn().mockResolvedValue({
      rank: "gold",
    }),
  };

  testConfig = {
    ...config,
  };

  client = createTestClient(testConfig);

  uid = "550e8400-e29b-41d4-a716-446655440000";
});

describe("GET /api/v1/members/:uid/rank", () =&gt; {
  it("正常終了すること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.members[":uid"].rank.$get({
      param: {
        uid: uid,
      },
    });

    // 検証
    expect(response.status).toBe(200);
    expect(calculateMemberRankUseCase.execute).toHaveBeenCalledTimes(1);
    expect(calculateMemberRankUseCase.execute).toHaveBeenCalledWith(
      expect.anything(),
      {
        uid,
      },
    );
  });

  it("uidがUUID形式ではない場合にバリデーションエラーになること", async () =&gt; {
    // テスト実行
    const response = await client.api.v1.members[":uid"].rank.$get({
      param: {
        uid: "invalid-uid",
      },
    });

    // 検証
    expect(response.status).toBe(400);
    expect(calculateMemberRankUseCase.execute).not.toHaveBeenCalled();
  });
});</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「src/presentation/http/routesNode.ts」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/routesNode.ts"><code>import { Hono } from "hono";
import type { NodeContainer } from "../../di/nodeContainer.js";
import { createUserRoutes } from "../http/generic/user/routes.js";
import { createSampleRoutes } from "../http/supporting/sample/routes.js";
import { createMemberRoutes } from "./core/member/routes.js";

export const createRoutesNode = (container: NodeContainer) =&gt; {
  /******************************
  * v1用のルーティング
  ******************************/
  // Honoでルーティングを作る際は、メソッドチェーン推薦
  const v1 = new Hono()
    .route(
      "/sample",
      createSampleRoutes(
        container.sampleHelloUseCase,
        container.sampleTextUseCase,
      ),
    )
    .route(
      "/users",
      createUserRoutes(
        container.createUserUseCase,
        container.findUsersUseCase,
        container.findUserUseCase,
        container.updateUserUseCase,
        container.deleteUserUseCase,
      ),
    )
    .route(
      "/members",
      createMemberRoutes(container.calculateMemberRankUseCase),
    );

    /******************************
    * ルーティング設定
    ******************************/
    return new Hono().route("/v1", v1);
};

</code></pre>
</div>
<p>&nbsp;</p>
<p>次にカスタムエラーを登録するため、「src/presentation/http/error/errorStatusMap.ts」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/presentation/http/error/errorStatusMap.ts"><code>import type { ContentfulStatusCode } from "hono/utils/http-status";
import { memberErrorStatusMap } from "../../../core/member/usecase/errors.js";
import { userErrorStatusMap } from "../../../generic/user/errors.js";

// 共通エラーコードとHTTPステータスコードのマッピング
export const errorStatusMap: Record&lt;string, ContentfulStatusCode&gt; = {
  ...userErrorStatusMap,
  ...memberErrorStatusMap,
};</code></pre>
</div>
<p>&nbsp;</p>
<h3>インテグレーションテストを作る</h3>
<p>次にインテグレーションを作るため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/tests/integration/core/member
$ touch src/tests/integration/core/member/member.test.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「src/tests/integration/core/member/member.test.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/tests/integration/core/member/member.test.ts"><code>import { testClient } from "hono/testing";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { DbPg } from "../../../../infrastructure/database/drizzle/pg/client.js";
import { createDbPg } from "../../../../infrastructure/database/drizzle/pg/client.js";
import { users } from "../../../../infrastructure/database/drizzle/pg/schema/users.js";
import { createNodeApp } from "../../../../runtime/node/app.js";
import type { NodeConfig } from "../../../../runtime/node/config.js";
import { config } from "../../../../runtime/node/config.js";

// ログ出力の無効化
vi.spyOn(console, "log").mockImplementation(() =&gt; {});

// テスト用クライアント作成関数
const createTestClient = (config: NodeConfig) =&gt; {
  return testClient(createNodeApp(config));
};

// テスト用クライアント変数
let client: ReturnType&lt;typeof createTestClient&gt;;

let testDbPg: DbPg;

// テスト用コンフィグ変数
let testConfig: NodeConfig;

// テスト前の共通セットアップ
beforeEach(() =&gt; {
  testConfig = {
    ...config,
    db: {
      ...config.db,
      name: "testing-db-pg",
    },
  };

  client = createTestClient(testConfig);
  testDbPg = createDbPg(testConfig.db);
});

// テスト後の共通クリーンアップ
afterEach(async () =&gt; {
  await testDbPg.db.delete(users);
  await testDbPg.pool.end();
});

describe("GET /api/v1/members/:uid/rank", () =&gt; {
  it("対象メンバーのランクを取得し、正常終了すること", async () =&gt; {
    // テストデータ登録
    const [resultUser] = await testDbPg.db
      .insert(users)
      .values([
        {
          lastName: "山田",
          firstName: "太郎",
          totalPurchaseAmount: 10_000,
        },
      ])
      .returning();

    // テスト実行
    const response = await client.api.v1.members[":uid"].rank.$get({
      param: {
        uid: resultUser.uid,
      },
    });

    // 検証
    expect(response.status).toBe(200);

    // レスポンス結果の検証
    const body = await response.json();
    expect(body).toEqual({
      rank: "silver",
    });
  });
});</code></pre>
</div>
<p>&nbsp;</p>
<h3>各種コードチェック</h3>
<p>次に以下のコマンドを実行し、各種コードチェックします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm run format
$ pnpm run lint
$ pnpm run test</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のように全てのテストがパスすればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-34.png" alt="" width="1200" height="634" class="aligncenter size-full wp-image-20735" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-34.png 1200w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-34-300x159.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-34-1024x541.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-34-768x406.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" />
<p>&nbsp;</p>
<h3>合計購入金額から会員ランクを出力するAPIを試す</h3>
<p>次に作成したAPIを試すため、まずは上記で作成したユーザー作成APIを利用し、「totalPurchaseAmount」を「10000」にして新規ユーザー作成後、レスポンス結果の「uid」をメモします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-32.png" alt="" width="2544" height="1582" class="aligncenter size-full wp-image-20731" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-32.png 2544w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-32-300x187.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-32-1024x637.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-32-768x478.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-32-1536x955.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-32-2048x1274.png 2048w" sizes="auto, (max-width: 2544px) 100vw, 2544px" />
<p>&nbsp;</p>
<p>次にGETメソッドで「http://localhost:3000/api/v1/members/:uid/rank」を実行後、以下のように正常終了し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-33.png" alt="" width="2542" height="1580" class="aligncenter size-full wp-image-20732" srcset="https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-33.png 2542w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-33-300x186.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-33-1024x636.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-33-768x477.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-33-1536x955.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/10/hono-sample-33-2048x1273.png 2048w" sizes="auto, (max-width: 2542px) 100vw, 2542px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はHono（TypeScript）でAPIを作成する方法をご紹介しました。</p>
<p>最近はインフラコストを抑えるために「Cloudflare Workers」の利用が注目されたりしていますが、Honoはマルチランタイムに対応しており、<span style="color: #3366ff;"><strong>Node.js環境だけでなく、Cloudflare Workersも含めた様々な環境にデプロイして利用できる点がメリット</strong></span>です。</p>
<p>加えて、バックエンド開発でもフロントエンド開発でよく利用されている<strong><span style="color: #3366ff;">TypeScriptに統一することで、エンジニア採用の間口を広げられるメリットもある</span></strong>ため、これからのバックエンド開発で利用するフレームワークとしては、第一候補になり得る可能性も高いです。</p>
<p>ただし、Honoはフルスタックフレームワークとは異なり、<strong><span style="color: #ff0000;">自由度が高いため、どのようなディレクトリ構成で作るのかなど、設計部分については難しい</span></strong>部分があります。</p>
<p>2026年以降で注目されているフレームワークには間違いないので、興味がある方はぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/substack"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2026/07/substack-start-3-486x290.jpg" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">Substackを始めました！注目のニュースレター（メルマガ）＆SNSサービス</p><span class="date gf">2026年7月2日</span></div></a></div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/hono-typescript-api">Hono入門｜TypeScriptでAPIを作成する方法【Node.js・Cloudflare対応】</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/hono-typescript-api/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>【スト6】モダンヤスミンの立ち回り・攻め方まとめ｜飛び道具対策・地上戦・起き攻め・リーサル</title>
		<link>https://tomoyuki65.com/sf6-modern-yasmin-strategy</link>
					<comments>https://tomoyuki65.com/sf6-modern-yasmin-strategy#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Mon, 21 Sep 2026 12:18:41 +0000</pubDate>
				<category><![CDATA[6. スト6]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20590</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 以前にモダンヤスミンの初心者向けコンボに関する記事を書きましたが、あれからランクマッチも行うようになり、一旦ハイマスターまで上げることができまし...</p>
The post <a href="https://tomoyuki65.com/sf6-modern-yasmin-strategy">【スト6】モダンヤスミンの立ち回り・攻め方まとめ｜飛び道具対策・地上戦・起き攻め・リーサル</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/09/yasmin-strategy-1.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-20618" srcset="https://tomoyuki65.com/wp-content/uploads/2026/09/yasmin-strategy-1.png 672w, https://tomoyuki65.com/wp-content/uploads/2026/09/yasmin-strategy-1-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>以前にモダンヤスミンの初心者向けコンボに関する記事を書きましたが、あれからランクマッチも行うようになり、<strong><span style="color: #3366ff;">一旦ハイマスターまで上げることができました。</span></strong></p>
<p>それによって、モダンヤスミンの理解も深められたので、この記事ではそんな<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>モダンヤスミンの立ち回り・攻め方について解説</strong></span>します。</p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/sf6-modern-yasmin-combo"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2026/08/sf6-yasmin-1-486x290.jpg" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">【スト6】モダンヤスミンの初心者向けコンボまとめ｜簡単で実戦向き</p><span class="date gf">2026年8月5日</span></div></a></div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>【スト6】モダンヤスミンの立ち回り・攻め方まとめ｜飛び道具対策・地上戦・起き攻め・リーサル</h2>
<h3>ヤスミンの特徴</h3>
<p>ヤスミンには<strong>「<span style="border-bottom: 2px solid #be3144;">バヤニ・モード</span>」</strong>という仕組みがあり、コンボなどで「ダロイ」や「タリム」といった必殺技をヒットさせると、バヤニ・モード状態にすることができます。</p>
<p>そして、バヤニ・モード状態であれば、生ドライブラッシュ（生DR）を使ってコンボダメージを伸ばすことができるようになるため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ヤスミンの基本的な戦い方としては「バヤニ・モードにする → コンボをする（生DRでコンボダメージを伸ばす）」</strong></span>になります。</p>
<p>また、ヤスミンはいわゆる<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ハイスタンダードキャラ</strong></span>であり、<strong><span style="color: #ff0000;">一般的な格闘ゲームのセオリーを踏まえながら立ち回らないと勝つのが難しい</span></strong>キャラになっています。</p>
<p>例えばJP、ダルシム、本田のように、「これをやっていれば勝ちやすい」みたいな自分の強みを押し付けて、対策できてない人に勝ちやすいというようなキャラとは異なり、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>丁寧な立ち回りが求められる</strong></span>ため、特に<span style="color: #ff0000;"><strong>初心者の方がこのキャラを使って勝つのは難しい</strong></span>です。</p>
<p>そのため、ヤスミンで勝ちたい場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>格闘ゲームのセオリーを踏まえて立ち回りながら、しっかり『読み合い』をしていくのが大事</strong></span>になります。</p>
<p>特に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>『読み合い』</strong></span>の部分については、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>格闘ゲームを楽しみながらプレイしていく上でも必要不可欠</strong></span>であるため、よくわかっていない方は以下の関連動画を参考にしてみて下さい。</p>
<p>&nbsp;</p>
<h3>『読み合い』に関する関連動画</h3>
<p>【スト6】ランクマで勝てないのが辛い人へ。格ゲーの面白さは「読み合い」【初心者】</p>
<p><iframe loading="lazy" title="【スト6】ランクマで勝てないのが辛い人へ。格ゲーの面白さは「読み合い」【初心者】" width="728" height="410" src="https://www.youtube.com/embed/GvwHwOuzshA?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<p>&nbsp;</p>
<h3>格闘ゲームのセオリーについて</h3>
<p>一般的な格闘ゲームのセオリーについて、詳細は割愛しますが、<strong><span style="color: #ff0000;">地上戦の部分においては「差し込み」、「置き」、「差し返し」があり、それらを上手く循環させながら読み合いを回していく必要</span></strong>があります。</p>
<p>加えて、スト6にはドライブゲージというシステムもあり、<strong><span style="color: #ff0000;">「ドライブラッシュ」を使えば一般的な格闘ゲームのセオリーを無視するような形で攻めることもできる</span></strong>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「飛びからの攻撃」、「地上戦」、「ドライブラッシュでの攻め」を織り交ぜながら、相手との駆け引きをしていくのが大事</strong></span>なゲームになっています。</p>
<p>&nbsp;</p>
<h3>立ち回りについて</h3>
<p>立ち回りの結論的な話として、一番大事なこととしては、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>自分がやれる選択肢をできるだけ多く持っておいて、相手に合わせてそれらを使い分け、相手に通る選択肢を多めに使っていくこと</strong></span>です。</p>
<p>&nbsp;</p>
<h3>立ち回り：飛びからの攻め</h3>
<p>まず<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>一番やりたいこととしてはリターンが高い「飛びからの攻め」</strong></span>で、相手が対空をあまりしてこないなら、ガンガン飛んでジャンプ攻撃からのコンボを狙うのが大事です。</p>
<p>ただし、<strong><span style="color: #ff0000;">相手がモダン操作の場合はワンボタン対空が可能であり、簡単に対空されてしまう</span></strong>ため、その場合は安直に「飛びからの攻め」はしないようにします。</p>
<p><span style="color: #ff0000;"><strong>相手がクラシック操作の場合は、対空が出る人と出ない人がいる</strong></span>ため、対空が出ない人に対しては、「飛びからの攻め」を多くした方がリターンが高い可能性があります。</p>
<p>&nbsp;</p>
<h3>立ち回り：飛び道具対策</h3>
<p>対戦相手に飛び道具キャラが来ることが多く、その場合はヤスミン側は相手の飛び道具を捌いて攻める必要があるため、<strong><span style="color: #ff0000;">基本的な飛び道具対策は知っておく必要</span></strong>があります。</p>
<div class="supplement boader">
<ul>
<li> <strong>前歩きしゃがみガードを繰り返し、少しずつ距離を詰める</strong></li>
<li><strong> 普通のガードだけだと近づくのが難しいため、適宜ジャストパリィも狙いながら、ジャンプ攻撃が届く間合いぐらいまで距離を詰める </strong></li>
<li><strong> ジャンプ攻撃が届く間合いになったら、飛び道具を撃つのにリスクが生まれるため、そこから読み合い（ジャンプ攻撃、ジャスパからジャンプ攻撃、ジャスパから生ドライブラッシュで攻め、リーチと発生が優秀な牽制技で飛び道具の発生を潰す、相手が飛び道具を撃たないなら地上戦）をする</strong></li>
</ul>
</div>
<p>&nbsp;</p>
<h3>立ち回り：地上戦について</h3>
<h4>・差し返しへの対応</h4>
<p>地上戦について、相手が上手い場合は、こちらが前に歩くとその分後ろに下がり、こちらの技を空振りさせるように後ろに引きながら<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「差し返し」</strong></span>を狙ってきます。</p>
<p>そんな「差し返し」を狙う相手には、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>深めに歩いてしゃがみ中足、前ステップからのしゃがみ中足で「差し込む」のが有効</strong></span>です。</p>
<p>その際、<span style="color: #3366ff;"><strong>しゃがみ中足からキャンセルドライブラッシュすることで攻めることができます</strong></span>が、<strong><span style="color: #ff0000;">しゃがみ中足がガードされていた場合、次に振る技がしゃがみ弱パンチじゃないと、相手の無敵技で切り返される可能性があるので注意</span></strong>が必要です。</p>
<p>そのため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>差し込む場合はしゃがみ中足がヒットしたのか、ガードされたのかをヒット確認するのが大事</strong></span>になります。</p>
<p>&nbsp;</p>
<h4>・置きへの対応</h4>
<p>次に<strong><span style="color: #ff0000;">こちらの「差し込み」を嫌がる相手は、こちらの前歩きや前ステップに対して技を「置く」（牽制技を空振りして近づかせないようにする）</span></strong>ため、<strong><span style="border-bottom: 2px solid #be3144;">こちらが前に出るのに対して技を置いてくるなら、逆に「差し返し」が狙えます。</span></strong></p>
<p>モダンヤスミンであれば、<span style="color: #3366ff;"><strong>しゃがみながら最速で立ち強パンチ（アシスト強攻撃）を出せるのが強みの一つ</strong></span>であるため、前歩きガードで様子見し、相手が技を空振った時に立ち強パンチを出すことで、差し返しが狙いやすかったりします。</p>
<p>また、発生が早くてリーチも長めの<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>立ち弱キック（弱攻撃）</strong></span>や、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>しゃがみ強キック（前斜め下 + 強攻撃）</strong></span>でも差し返しを狙うことも可能です。</p>
<p>特に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>しゃがみ強キックで差し返せるとダウンを取ることができ、生ドライブラッシュから起き攻めを狙えたりするので有効</strong></span>です。</p>
<p>&nbsp;</p>
<h4>・差し込みへの対応</h4>
<p>次にこちらの「差し返し」に対して、相手は「差し込み」を狙ってくるため、その場合は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>技を「置く」ことで相手の前進行動を抑制</strong></span>できます。</p>
<p>「置き」として技を振る場合、空振りするのが前提のため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>もっともリスクが小さいのは「しゃがみ弱パンチ（しゃがみ弱攻撃）」による置き</strong></span>です。</p>
<p>または、<strong><span style="color: #ff0000;">もう少しリーチが欲しければ立ち弱キック（立ち弱攻撃）を使う必要</span></strong>がありますが、<strong><span style="color: #ff0000;">相手に差し返しされやすい</span></strong>ため、その点は注意が必要です。</p>
<p>また、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>相手がしゃがみ中キックなどの下段攻撃で差し込みを狙ってくる場合、立ち強キック（立ち強攻撃）なら下段攻撃をすかし、ヒット時にはパニッシュカウンターになる</strong></span>ため、その場合は生ドライブラッシュからコンボもできるため、高いリターンを狙えます。</p>
<p>ただし、<strong><span style="color: #ff0000;">立ち強キック</span></strong>は相手のガードにめり込むと確定反撃を食う可能性があるのと、発生が遅くて相手の中攻撃などに潰されやすかったり、インパクトにも弱いため、<strong><span style="color: #ff0000;">振り過ぎには注意</span></strong>しましょう。</p>
<p>&nbsp;</p>
<h3>立ち回り：強タリムで攻め</h3>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>相手が守備的で技をあまり降ってこない場合、単発で「強タリム」から攻めるのも有効</strong></span>です。</p>
<p>ガードされても+1f有利であり、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>相手が小技暴れしてくるなら「しゃがみ弱攻撃 → アシスト弱攻撃」による暴れ潰し、何もしてこないなら投げを狙う</strong></span>ように択をかけれます。</p>
<p>ただし、<strong><span style="color: #ff0000;">「強タリム」は発生が遅く、相手の技に潰されることも多いため、そこは読み合いしていく必要</span></strong>があります。</p>
<p>&nbsp;</p>
<h3>立ち回り：ランギットは択として持っておく</h3>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>しゃがみSP（ランギット）</strong></span>を使えば上空から攻めれます（弱、中、強で距離が違うので使い分け）が、<span style="color: #ff0000;"><strong>あまり強い技ではないため、基本的には使いません</strong></span>が、<span style="color: #3366ff;"><strong>相手の意識を散らすための手札として持っておく価値はあり</strong></span>ます。</p>
<p>また、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>しゃがみSP後に「↓ + 弱 or 中 or 強」の入力でめくり攻撃を狙えます</strong></span>が、こちらも<span style="color: #ff0000;"><strong>別に強くはない</strong></span>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>画面端から脱出したい場合の選択肢の一つ</strong></span>として覚えておくといいです。</p>
<p>&nbsp;</p>
<h3>立ち回り：ヤスミン対策への対応</h3>
<p>相手のヤスミン対策の基本としては、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>何か攻撃されたら少し後ろに下がる</strong></span><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>こと</strong></span>なので、そんな<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>相手が後ろに下がりたいポイントがあった場合、しゃがみ弱キック（アシスト弱攻撃）や前ステップからの攻めを狙うのも有効</strong></span>です。</p>
<p>&nbsp;</p>
<h3>立ち回り：生DRで択をかける</h3>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>スト6には「ドライブラッシュ」というシステムがある</strong></span>ため、上記で説明したような地上戦をある種無視したような形で攻めて、<span style="color: #3366ff;"><strong>一気に密着状態を作って読み合いの展開にする</strong></span>ことができます。</p>
<p>そのため、主に地上戦の間合いで、相手が技を置いてこなかったり、飛び道具も撃ってこない場合、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>生ドライブラッシュで択をかけにいくのも大事</strong></span>です。</p>
<p>ただし、<strong><span style="color: #ff0000;">生ドライブラッシュはリーチが長い技や飛び道具の置きに弱く、ラッシュ後は一定時間無防備な時間がある</span></strong>ため、生ドライブラッシュを見てから無敵技で切り返されたり、発生やリーチが長い通常技で止められたりする可能性はあるので注意しましょう。</p>
<p>&nbsp;</p>
<h4>・生ドライブラッシュ時に無敵技で切り返される場合</h4>
<p>生ドライブラッシュ時に無敵技で切り返される場合、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ラッシュ後に最速でしゃがみ強パンチ（しゃがみ弱攻撃）や投げをする</strong></span>ことで、ラッシュを止めて<span style="color: #3366ff;"><strong>相手の無敵技を釣ることが可能</strong></span>です。</p>
<p>加えて、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ラッシュ後に何もせずガードをしておく</strong></span>と、ラッシュを伸ばして止まった時にガードになるため、これで無敵技を釣ることもあります。</p>
<p>&nbsp;</p>
<h4>・生ドライブラッシュ時に優秀な通常技で止められる場合</h4>
<p>生ドライブラッシュ時に、<span style="color: #ff0000;"><strong>発生やリーチが優秀な通常技（立ち弱キックなど）でラッシュを止められることもあります</strong></span>が、これは<strong><span style="color: #ff0000;">間合いによって使うべき通常技を変える必要があったり、簡単ではなく、もちろんリスクもある</span></strong>ため、その点は覚えておきましょう。</p>
<p>このように、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>技を置いてラッシュを止めてくる場合は、間合いにはよるものの、ヤスミンなら生ドライブラッシュ強パンチ（アシスト強攻撃）</strong></span>が突進技として優秀で、<strong><span style="color: #3366ff;">ヒットすればコンボも可能</span></strong>です。</p>
<p>&nbsp;</p>
<h4>・生ドライブラッシュ時にガードされる場合</h4>
<p>生ドライブラッシュを無敵技や通常技で止めるのは、相手側にもリスクがあるため、<span style="color: #ff0000;"><strong>上手い相手ほど普通にガードして安定的な守りをする</strong></span>ようになります。</p>
<p>その場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>生ドライブラッシュから中段攻撃（→ + 中攻撃）</strong></span>でしゃがみガードを崩し、ヒットすればコンボが可能です。</p>
<p>ラッシュ中段を<span style="color: #3366ff;"><strong>ガードされても+1f有利</strong></span>ですが、<strong><span style="color: #ff0000;">投げ間合いではない</span></strong>ので、その点は注意しましょう。</p>
<p>また、相手が立ちガードをしてラッシュ中段が通らない場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>生ドライブラッシュから下段攻撃（しゃがみ弱キックやしゃがみ中キック）</strong></span>で立ちガードを崩し、ヒットすればコンボが可能です。</p>
<p>尚、<span style="color: #3366ff;"><strong>ヤスミンのラッシュしゃがみ弱キック（アシスト弱攻撃）はガードされても+2f有利で、シミー（投げ抜け狩り）を狙うことも可能</strong></span>です。</p>
<p>このように、生ドライブラッシュから中段攻撃か下段攻撃を狙えば、相手のガードを崩せる可能性があります。</p>
<p>&nbsp;</p>
<h4>・生ドライブラッシュ中段攻撃を小技で止められる場合</h4>
<p>生ドライブラッシュ中段は強いですが、<strong><span style="color: #ff0000;">発生が遅いのが弱点で、相手の小技（しゃがみ弱パンチなど）で止められる</span></strong>ことがあります。</p>
<p>この場合は、こちらも<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>生ドライブラッシュしゃがみ弱パンチ（しゃがみ弱攻撃）などの発生の早い技で攻めるのが安定</strong></span>です。</p>
<p><span style="color: #3366ff;"><strong>ヤスミンのラッシュしゃがみ弱パンチ（しゃがみ弱攻撃）はガードされて+3f有利</strong></span>で、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>シミー（投げ抜け狩り）と投げの択</strong></span>になります。</p>
<p>ただし、<span style="color: #ff0000;"><strong>密着しすぎているとシミーが狙えない場合がある</strong></span>ので、その場合は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>しゃがみ弱パンチをガードされた後に垂直ジャンプをする</strong></span>と、空ジャンプによるシミーを狙えます。</p>
<p>空ジャンプによるシミーの場合、その後の<span style="color: #ff0000;"><strong>ジャンプ攻撃がガードされることがあったり、ジャストパリィを取られることもあります</strong></span>が、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>パリィをしてくる相手には空ジャンプ後に投げが通るため、読み合い</strong></span>になります。</p>
<p>&nbsp;</p>
<h4>・生ドライブラッシュ強パンチ（アシスト強攻撃）の弱点</h4>
<p>ヤスミンの生ドライブラッシュ強パンチ（アシスト強攻撃）は、<strong><span style="color: #ff0000;">ジェイミーの突進技に近い性質があり、一つの対策としては垂直ジャンプを置くこと</span></strong>です。</p>
<p>そのため、相手が対策として垂直ジャンプを置いてくる場合は、生ドライブラッシュ強パンチ（アシスト強攻撃）は使わないようにし、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>生ドライブラッシュしゃがみ弱パンチ（しゃがみ弱攻撃）などを狙って、相手が垂直ジャンプをしていたら対空技（モダンヤスミンなら「→ + SP」）で落とすのが対策</strong></span>になります。</p>
<p>&nbsp;</p>
<h4>・生ドライブラッシュの読み合いを拒否したい場合</h4>
<p><span style="color: #ff0000;"><strong>生ドライブラッシュからの攻めについては、いずれにせよ読み合いになり、守るのは難しい</strong></span>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>拒否したい場合はバックジャンプが安定</strong></span>だったりするので、その点は覚えておきましょう。</p>
<p>&nbsp;</p>
<h3>立ち回り：パンギルについて</h3>
<p>ヤスミンは一応<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>飛び道具</strong></span>を持っており、モダンヤスミンなら<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「↓↓ + 弱攻撃 or 中攻撃 or 強攻撃」</strong></span>で出せるほか、OD版もあります。</p>
<p>ただし、<strong><span style="color: #ff0000;">いずれにせよ発生が遅く、気軽に使える技ではない</span></strong>ため、立ち回りの中ではほぼ使いませんが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>相手がガンガン後ろに下がってくタイプであれば撃つ暇がある</strong></span>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>強パンギル（↓↓ + 強攻撃）で遠くに飛ばして、その後に生ドライブラッシュで攻めを狙うのは可能</strong></span>です。</p>
<p><span style="color: #ff0000;">※生ドライブラッシュで攻める際に、無敵技で切り返されることも多いので、そこはしっかり読み合いすること。</span></p>
<p>&nbsp;</p>
<h4>・バヤニ・モード中のコンボの〆でパンギルによる択をかけれる</h4>
<p>主な使い所の一つとしては、バヤニ・モード中のコンボ「何かコンボから強ダロイ（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ + 強攻撃） → 生ドライブラッシュ → しゃがみ中パンチ（アシスト中攻撃） → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強攻撃）→ 立ち強パンチ（アシスト強攻撃）」の〆として、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>立ち強パンチ（アシスト強攻撃）後にキャンセルでパンギル（主に弱版を使う）を出すことができるため、択をかけることが可能</strong></span>です。</p>
<p>その場合、相手の小技暴れには「しゃがみ弱パンチ（しゃがみ弱攻撃）からのコンボ」、ガードで固まるなら「投げ」、無敵技での切り返しには「ガードから反撃」を狙います。</p>
<p>ただし、<span style="color: #ff0000;"><strong>密着の読み合いになっており、シミーはできない（ガードしてると投げられる）</strong></span>ため、無敵技を読んでガードをする場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>投げ抜けも意識</strong></span>しておいた方がいいです。</p>
<p>尚、立ち強パンチ（アシスト強攻撃）後にキャンセルで出す技は、強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強攻撃）とパンギルで選べ、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>初心者は強リパで〆るのが簡単（わかりやすい）</strong></span>ですが、<span style="color: #ff0000;"><strong>ハイスタンダードキャラでワンパターン行動は弱い</strong></span>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>慣れてきたらパンギルの方で読み合いしていくのも大事</strong></span>です。</p>
<p>&nbsp;</p>
<h4>・起き攻めの展開で相手を揺さぶる</h4>
<p>もう一つの使い所としては、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>起き攻めの場面</strong></span>になりますが、<strong><span style="color: #ff0000;">相手がその場受け身をしていた場合はラッシュ攻撃やインパクトなどで確定反撃を貰う</span></strong>ため、その点は注意しましょう。</p>
<p>&nbsp;</p>
<h3>立ち回り：ラインとゲージ管理について</h3>
<p>スト6では、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>1コンボで画面端に連れていかれることも多いゲーム</strong></span>ですが、<span style="color: #ff0000;"><strong>格闘ゲームにおける画面端は弱く、相手に有利な読み合いの展開になる</strong></span>ため、その点は注意しましょう。</p>
<p>対戦において、たまにガンガン後ろに下がっていくような人もいますが、立ち回りとしても後ろに下がりすぎるのは弱い行動です。</p>
<p>基本的には<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>画面の中央付近で戦うようにしたり、中央より前気味に戦えるといい</strong></span>です。</p>
<p>逆に後ろに下がらなきゃいけない場面としては、<strong><span style="color: #ff0000;">ドライブゲージが少なくなって回復させたい場面</span></strong>になり、その場合は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>少しずつ後ろに下がりながら時間を稼ぐ</strong></span>のが大事です。</p>
<p>もちろん、差し返しを狙うために後ろに下がりすぎる（相手が前に来る分下がるため）こともありますが、そういう場合は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>技を置いて読み合いするのも大事</strong></span>なので、<span style="color: #ff0000;"><strong>画面端に行きすぎないようには注意</strong></span>しましょう。</p>
<p>&nbsp;</p>
<h4>・中足キャンセルラッシュ後はドライブゲージが減るので注意</h4>
<p>差し込みで<strong><span style="color: #ff0000;">しゃがみ中キック（しゃがみ中攻撃）キャンセルドライブラッシュを使うと、ドライブゲージを３ゲージ消費してしまう</span></strong>ため、<strong><span style="color: #ff0000;">その攻めが通らなかった場合にバーンアウトしやすくなるというリスク</span></strong>があります。</p>
<p>そのため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>中足キャンセルラッシュの攻めが通らなかった場合は、少しずつ後ろに下がって時間を稼ぎ、ドライブゲージを回復させるように立ち回るのも大事</strong></span>です。</p>
<p>ただし、<strong><span style="color: #ff0000;">ヤスミンは気軽に撃てる飛び道具がなく、他の飛び道具キャラ（リュウ、ガイルなど）のようにドライブゲージを回復させるのが簡単ではない</span></strong>ため、その点も注意しましょう。</p>
<p>尚、上記のことも踏まえ、ヤスミンにおけるドライブゲージの使い方としては、中足ラッシュより、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>中足ODダロイの方を狙った方がいい可能性もある</strong></span>ので覚えておきましょう。</p>
<p>&nbsp;</p>
<h3>画面端で強リパ（対空技）後の起き攻めについて</h3>
<p>コンボで画面端に近い場合に、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強攻撃）で〆た後は起き攻めの展開</strong></span>になりますが、こちらの場合は<span style="color: #3366ff;"><strong>シミーが可能</strong></span>です。</p>
<p>また、<span style="color: #ff0000;"><strong>起き攻めの展開ではしゃがみガードされることが多い</strong></span>ため、<span style="color: #3366ff;"><strong>素直に中段（→ + 中攻撃）を重ねるのも有効</strong></span>です。（「中段→投げ」などが通りやすい）</p>
<p>そのほか、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>小技で暴れてくる場合などについては、しゃがみ中パンチ（アシスト中攻撃）を２回重ねてヒット確認</strong></span>し、ヒットしてたらコンボで伸ばすのも使ったりします。</p>
<p>&nbsp;</p>
<h4>・柔道もある</h4>
<p>ヤスミンは<span style="color: #3366ff;"><strong>柔道（投げの後に投げが重なる）もある</strong></span>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>投げた後に最速で「前ステップ → 少し歩く → 投げ」をする</strong></span>と、柔道できます。（相手の最速弱パンチ暴れに勝てる）</p>
<p>ただし、フレーム状況はそれほどいいわけではなく、<strong><span style="color: #ff0000;">バックステップや前ジャンプに弱く、打撃を重ねる場合もそれほどダメージを取れなかったりする</span></strong>ので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>リスクを考えながら読み合いする必要</strong></span>があります。</p>
<p>&nbsp;</p>
<h4>・画面端では鳥かごが基本</h4>
<p>画面端で起き攻めをしない場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>鳥かご（画面端から脱出させないように封じ込める立ち回り）が有効</strong></span>なので、無理に前に攻めずに、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>相手が飛んでくるなら対空、ガードするならドライブゲージを削りやすい技をガードさせる（ヤスミンなら立ち強キック）、ラッシュで突進してくるなら無敵技（ヤスミンならODリパ or SA1 or SA3）で切り返す</strong></span>、という感じで画面を維持しながら戦えると、<span style="color: #3366ff;"><strong>有利な読み合いが可能</strong></span>です。</p>
<p><span style="color: #ff0000;">※画面端の相手は後ろに下がって差し返すことができないため、不利な読み合いを強いられます。</span></p>
<p>&nbsp;</p>
<h3>SA2 について</h3>
<p>ヤスミンはSA2を使うと、SAゲージが消費しきるまでバヤニ・モード状態で戦うことができ、さらにSA2中のみ使える必殺技（リニャ・ン・リワナグ「← + 中攻撃 + 強攻撃」）もあったりします。</p>
<p>ただし、<strong><span style="color: #ff0000;">SA2を上手く使っていくのはとても難しい</span></strong>ため、<strong><span style="border-bottom: 2px solid #be3144;">基本的にはSA1やSA3のためにゲージを使うようにした方がいい</span></strong>です。</p>
<p>尚、<span style="color: #ff0000;"><strong>もしSA2を使うのであれば、相手のドライブゲージが少なく、SA2からの連携で相手のドライブゲージを削ってバーンアウトさせられるような状況</strong></span>になります。</p>
<p>&nbsp;</p>
<h3>SA3 弾抜けについて</h3>
<p>ヤスミンは飛び道具キャラが相手だと立ち回りが難しくなりますが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>SA3を使えば弾抜けが狙えます。</strong></span></p>
<p>特に<strong><span style="color: #3366ff;">モダンヤスミンならワンボタンSA3を出せるのが強み</span></strong>で、クラシック操作より<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>弾抜けを狙いやすい</strong></span>ため、その点は活かしましょう。</p>
<p>&nbsp;</p>
<h3>常時バヤニモードの設定について</h3>
<p>トレーニングモードでは、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>設定メニュー「環境設定 → キャラクター固有設定」から、ヤスミンのバヤニ・モードを「発動状態」にすることが可能</strong></span>です。</p>
<p>これにより、バヤニ・モード状態からのコンボ練習を効率的にできるようになります。</p>
<p>&nbsp;</p>
<h3>キャラ対策・リーサルについて</h3>
<p>ヤスミンの立ち回りや攻め方の基本としては上記の通りになりますが、<strong><span style="color: #ff0000;">他には個別にキャラクター対策をしないと、わからん殺しをされることも多い</span></strong>です。</p>
<p>また、勝ち負けを大きく左右するものとして<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「リーサル」</strong></span>があり、これは<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>１コンボで相手を倒し切ること</strong></span>を指します。</p>
<p>例えば相手の体力ゲージが5割ぐらいの時に、SA3を絡めたコンボをして倒し切れるような状態であれば、それを狙っていくのが大事になります。</p>
<p>特に強い相手と対戦するとよく起こりますが、<strong><span style="color: #ff0000;">相手の体力が後ちょっとだったとしても、相手にリーサルを決められて負けてしまうことも多いゲームになっている</span></strong>ため、相手がリーサルをしてくるなら、こちらもリーサルを決めないと、勝ち切るのが難しいです。</p>
<p>&nbsp;</p>
<h3>関連動画</h3>
<p>【スト6】モダンヤスミンの立ち回り・攻め方を解説！｜飛び道具対策・地上戦・起き攻め・リーサル</p>
<p><iframe loading="lazy" title="【スト6】モダンヤスミンの立ち回り・攻め方を解説！｜飛び道具対策・地上戦・起き攻め・リーサル" width="728" height="410" src="https://www.youtube.com/embed/PCRGCDaBI9k?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はモダンヤスミンの立ち回り・攻め方について解説しました。</p>
<p>ヤスミンで勝ちたい場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>自分がやれる選択肢を多くすること、相手に合わせてしっかり読み合いをしていくこと、そして狙えるところでしっかりリーサルを狙うのが大事</strong></span>です。</p>
<p><strong><span style="color: #ff0000;">現状は相対的に見ると評価は高くなく、どちらかというと弱い寄りの位置にいる</span></strong>のは残念ですが、<strong><span style="color: #3366ff;">ハイスタンダードキャラなので、慣れると使いやすい（使いやすさは高い）</span></strong>キャラになっています。</p>
<p>そんな感じで、モダンヤスミンの立ち回りや攻め方を知りたかった方は、ぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>
<p style="text-align: center;"><strong>＼ よければXのフォロー・YouTubeのチャンネル登録もぜひお願いします！ ／</strong></p>
<div class="supplement boader">
<p>・Xのフォローは<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener">こちら</a></p>
<p>・YouTubeのチャンネル登録は<a href="https://www.youtube.com/channel/UCehXknUVdKmYct3r_ecqwLw?sub_confirmation=1" target="_blank" rel="noopener">こちら</a></p>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/sf6-modern-yasmin-strategy">【スト6】モダンヤスミンの立ち回り・攻め方まとめ｜飛び道具対策・地上戦・起き攻め・リーサル</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/sf6-modern-yasmin-strategy/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>【スト6】モダンヤスミンの初心者向けコンボまとめ｜簡単で実戦向き</title>
		<link>https://tomoyuki65.com/sf6-modern-yasmin-combo</link>
					<comments>https://tomoyuki65.com/sf6-modern-yasmin-combo#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Wed, 05 Aug 2026 01:13:03 +0000</pubDate>
				<category><![CDATA[6. スト6]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20555</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 2026年8月3日よりストリートファイター6のYEAR4に突入し、新キャラ「ヤスミン（Yasmine）」が追加されました！ YEAR4に合わせて...</p>
The post <a href="https://tomoyuki65.com/sf6-modern-yasmin-combo">【スト6】モダンヤスミンの初心者向けコンボまとめ｜簡単で実戦向き</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/08/sf6-yasmin-1.jpg" alt="" width="672" height="480" class="aligncenter size-full wp-image-20569" srcset="https://tomoyuki65.com/wp-content/uploads/2026/08/sf6-yasmin-1.jpg 672w, https://tomoyuki65.com/wp-content/uploads/2026/08/sf6-yasmin-1-300x214.jpg 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>2026年8月3日よりストリートファイター6のYEAR4に突入し、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>新キャラ「ヤスミン（Yasmine）」が追加</strong></span>されました！</p>
<p>YEAR4に合わせてキャラ調整なども行われ、私もこれからヤスミンを使っていこうかなと思っているので、またいつものように初心者向けのコンボを調べました。</p>
<p>この記事では、そんなモダンヤスミンの初心者向けコンボについてご紹介します。</p>
<p><span style="color: #ff0000;">※この記事はモダン操作のアシストコンボだけを卒業し、少しでも強くなりたい人向けの内容となっています。もちろんカジュアルに楽しみたい初心者の方はアシストコンボだけでも楽しめるので、お気軽にスト6を始めてみて下さい！</span></p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>【スト6】モダンヤスミンの初心者向けコンボまとめ｜簡単で実戦向き</h2>
<p><span style="color: #ff0000;">※下記コマンドのAはアシストボタン、DRはドライブラッシュ</span></p>
<p>&nbsp;</p>
<h3>バヤニ・モード無しの場合に狙うコンボ</h3>
<h4>・ジャンプ攻撃始動</h4>
<div class="supplement boader"><strong>ダメージ：2340</strong></p>
<p>ジャンプ強 or ジャンプA強（ヒット確認）→ しゃがみ強 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 前ステップ（→→）→ 前ステップ（→→）→ 起き攻め（+6f）</p>
</div>
<p><span style="color: #ff0000;">※アロイ後にバヤニ・モードになる。相手がその場受け身でも後方受け身でも+6fの起き攻めが可能。起き攻めの択としては、①投げ、②打撃重ね、④無敵技読みのガード</span></p>
<p>&nbsp;</p>
<div class="supplement "><strong>起き攻めの択の例</strong></p>
<ol>
<li> 投げ</li>
<li> 打撃重ね：A中（カウンター確認）→ 少し遅らせて単発のA中 → 強ダロイ → 派生のアロン → 生DR → しゃがみ中 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎←<span> </span>+ 強）→ A強 → 強リパ（→↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強</span>）or SA1 or SA3</li>
<li> 無敵技読みのガード（無敵技ガード後にパニッシュカウンター始動コンボ）</li>
</ol>
</div>
<p><span style="color: #ff0000;">※もし相手が後方受け身の場合、最速で後ろ微歩きするとシミー（投げ抜け狩り）も狙えます。これは余裕が出来てから狙うようにしてみて下さい。</span></p>
<p>&nbsp;</p>
<h4>・ジャンプ攻撃始動（画面端の場合）</h4>
<div class="supplement boader"><strong>ダメージ：2940</strong></p>
<p>ジャンプ強 or ジャンプA強（ヒット確認）→ しゃがみ強 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 弱リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 弱） → しゃがみ弱（空振りでフレーム消費） → 起き攻め（+4f）</p>
</div>
<p>&nbsp;</p>
<h4>・タリム始動</h4>
<div class="supplement boader"><strong>ダメージ：中タリム（1100）、強タリム（1200）</strong></p>
<p>中タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 中）or 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）</p>
</div>
<p><span style="color: #ff0000;">※ヒットするとバヤニ・モードになります。中タリムはヒット後+2fで攻め継続可能です。強タリムはヒット後にダウンするので生ラッシュなどから起き攻め可能です。また、中タリムはガードされて-3fなので相手のターンですが、強タリムはガードされて+1fなので攻め継続可能です。尚、どちらも発生が遅いので頻繁に使う技ではなく、相手が様子見してくることが多いならたまに使う感じがいいです。</span></p>
<p>&nbsp;</p>
<h4>・立ち回りからのA強始動のヒット時</h4>
<div class="supplement boader"><strong>ダメージ：2028</strong></p>
<p>A強（ヒット時）→ キャンセルDR → A強（フルヒット） → A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 前ステップ（→→）→ 前ステップ（→→）→ 起き攻め（+6f）</p>
</div>
<p><span style="color: #ff0000;">※モダンの場合は、しゃがみガードからもすぐにA強が出せるのが強みです。そしてモダンの技の中でリーチが長めの技になります。ドライブゲージがある時はA強キャンセルDRで攻められます。</span></p>
<p>&nbsp;</p>
<h4>・立ち回りからのA強始動のガード時</h4>
<div class="supplement boader">
<p>A強（ガード時）→ キャンセルDR → しゃがみ弱（ここまで連続ガード） → しゃがみ弱ガード後は+3fで攻め継続可能（ただし、無敵技で切り返される可能性があるので読み合い）、しゃがみA弱からアシストコンボで暴れ潰しも狙えます。</p>
</div>
<p>&nbsp;</p>
<h4>・差し込みのしゃがみ中始動のヒット時</h4>
<div class="supplement boader"><strong>ダメージ：1854</strong></p>
<p>前歩き or 前ステップ → しゃがみ中（ヒット時）→ キャンセルDR → A強（フルヒット） → A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 前ステップ（→→）→ 前ステップ（→→）→ 起き攻め（+6f）</p>
</div>
<p><span style="color: #ff0000;">※相手の後ろ下がりを読んだらしゃがみ中から崩せます。</span></p>
<p>&nbsp;</p>
<h4>・差し込みのしゃがみ中始動のガード時</h4>
<div class="supplement boader">
<p>前歩き or 前ステップ → しゃがみ中（ガード時）→ キャンセルDR → しゃがみ弱（ここまで連続ガード） → しゃがみ弱ガード後は+3fで攻め継続可能（ただし、無敵技で切り返される可能性があるので読み合い）、しゃがみA弱からアシストコンボで暴れ潰しも狙えます。</p>
</div>
<p>&nbsp;</p>
<h4>・生DRから下段始動のヒット時</h4>
<div class="supplement boader"><strong>ダメージ：1140</strong></p>
<p>生DR → A弱（ヒット時）→ A弱 → A弱 → A弱 → +1fで攻め継続可能（ただし、無敵技で切り返される可能性があるので読み合い）、しゃがみ弱キャンセルDRなどで暴れ潰しも狙えます。</p>
</div>
<p>&nbsp;</p>
<h4>・生DRから下段始動のガード時</h4>
<div class="supplement boader">
<p>生DR → A弱（ガード時）→ +2fで攻め継続可能（ただし、無敵技で切り返される可能性があるので読み合い）、しゃがみ弱キャンセルDRなどで暴れ潰しも狙えます。</p>
</div>
<p>&nbsp;</p>
<h4>・生DRから中段始動のヒット時</h4>
<div class="supplement boader"><strong>ダメージ：1940</strong></p>
<p>生DR → 前中（ヒット時）→ A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 前ステップ（→→）→ 前ステップ（→→）→ 起き攻め（+6f）</p>
</div>
<p><span style="color: #ff0000;">※相手のしゃがみガードを崩したい場合、ラッシュ中段が非常に有効です。</span></p>
<p>&nbsp;</p>
<h4>・生DRから中段始動のガード時</h4>
<div class="supplement boader">
<p>生DR → 前中（ガード時）→ +1fで攻め継続可能（ただし、無敵技で切り返される可能性があるので読み合い）、しゃがみ弱キャンセルDRなどで暴れ潰しも狙えます。</p>
</div>
<p>&nbsp;</p>
<h4>・立ち強パニッシュカウンター始動</h4>
<div class="supplement boader"><strong>ダメージ：強リパ〆（2535）、SA1〆（2694）、SA3〆（4094）、CA〆（4344）</strong></p>
<p>立ち強（パニッシュカウンター確認）→ 生DR → A中 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）or SA1 or SA3</p>
</div>
<p><span style="color: #ff0000;">※立ち強は発生が遅いものの、モダンの中でリーチが長い技。パニカン確認からコンボが狙えます。</span></p>
<p>&nbsp;</p>
<h4>・画面端インパクトの壁バウンド始動</h4>
<div class="supplement boader"><strong>ダメージ：1904</strong></p>
<p>画面端でインパクト（壁バウンド）→ しゃがみ強 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 弱リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 弱）→ しゃがみ弱（空振りでフレーム消費）→ 起き攻め（+4f）</p>
</div>
<p>&nbsp;</p>
<h4>・無敵技ガード後のパニッシュカウンター始動</h4>
<div class="supplement boader"><strong>ダメージ：2300</strong></p>
<p>無敵技ガード後 → A強（パニッシュカウンター）→ A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 前ステップ（→→）→ 前ステップ（→→）→ 起き攻め（+6f）</p>
</div>
<p>&nbsp;</p>
<h3>バヤニ・モード有りの場合</h3>
<h4>・ジャンプ攻撃始動</h4>
<div class="supplement boader"><strong>ダメージ：強リパ〆（3679）、SA1〆（3956）、SA3〆（5356）、CA〆（5606）</strong></p>
<p>ジャンプ強 or ジャンプA強（ヒット確認）→ しゃがみ強 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 生DR → A中 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）or SA1 or SA3</p>
</div>
<p><span style="color: #ff0000;">※SA3で〆る場合は、最後の強タリムの後に直接「ODダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 弱中 or 中強） → 派生のアロン → SA3」にしてダメージアップ可能です。（ダメージ：5806、CAなら6056）</span></p>
<p>&nbsp;</p>
<h4>・立ち回りからのA強始動のヒット時</h4>
<div class="supplement boader"><strong>ダメージ：強リパ〆（3059）、SA1〆（3439）、SA3〆（4839）、CA〆（5089）</strong></p>
<p>A強（ヒット時）→ キャンセルDR → A強（フルヒット） → A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 生DR → A中 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）or SA1 or SA3</p>
</div>
<p><span style="color: #ff0000;">※SA3で〆る場合は、最後の強タリムの後に直接「ODダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 弱中 or 中強） → 派生のアロン → SA3」にしてダメージアップ可能です。（ダメージ：5145、CAなら5395）</span></p>
<p>&nbsp;</p>
<h4>・差し込みのしゃがみ中始動のヒット時</h4>
<div class="supplement boader"><strong>ダメージ：強リパ〆（2570）、SA1〆（3068）、SA3〆（4468）、CA〆（4718）</strong></p>
<p>前歩き or 前ステップ → しゃがみ中（ヒット時）→ キャンセルDR → A強（フルヒット） → A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 生DR → A中 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）or SA1 or SA3</p>
</div>
<p><span style="color: #ff0000;">※SA3で〆る場合は、最後の強タリムの後に直接「ODダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 弱中 or 中強） → 派生のアロン → SA3」にしてダメージアップ可能です。（ダメージ：4612、CAなら4862）</span></p>
<p><span style="color: #ff0000;">※しゃがみ中始動の場合のみ、最後のA強の後に「ODダロイ（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ + 弱中 or 中強） → 派生のアロン → SA3」のようにすると、さらにダメージアップの模様。（ダメージ：4644、CAなら4894）</span></p>
<p>&nbsp;</p>
<h4>・生DRから下段始動のヒット時</h4>
<div class="supplement boader"><strong>ダメージ：強リパ〆（2360）、SA1〆（2740）、SA3〆（4140）、CA〆（4390）</strong></p>
<p>生DR → A弱（ヒット時）→ A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 生DR → A中 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）or SA1 or SA3</p>
</div>
<p><span style="color: #ff0000;">※SA3で〆る場合は、最後の強タリムの後に直接「ODダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 弱中 or 中強） → 派生のアロン → SA3」にしてダメージアップ可能です。（ダメージ：4446、CAなら4696）</span></p>
<p>&nbsp;</p>
<h4>・生DRから中段始動のヒット時</h4>
<div class="supplement boader"><strong>ダメージ：強リパ〆（3279）、SA1〆（3556）、SA3〆（4956）、CA〆（5206）</strong></p>
<p>生DR → 前中（ヒット時）→ A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 生DR → A中 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）or SA1 or SA3</p>
</div>
<p><span style="color: #ff0000;">※SA3で〆る場合は、最後の強タリムの後に直接「ODダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 弱中 or 中強） → 派生のアロン → SA3」にしてダメージアップ可能です。（ダメージ：5406、CAなら5656）</span></p>
<p>&nbsp;</p>
<h4>・無敵技ガード後のパニッシュカウンター始動</h4>
<div class="supplement boader"><strong>ダメージ：強リパ〆（3639）、SA1〆（3916）、SA3〆（5316）、CA〆（5566）</strong></p>
<p>無敵技ガード後 → A強（パニッシュカウンター）→ A中 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 生DR → A中 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）or SA1 or SA3</p>
</div>
<p><span style="color: #ff0000;">※SA3で〆る場合は、最後の強タリムの後に直接「ODダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 弱中 or 中強） → 派生のアロン → SA3」にしてダメージアップ可能です。（ダメージ：5766、CAなら6016）</span></p>
<p>&nbsp;</p>
<h3>画面端の場合はしゃがみ強を使うとダメージを伸ばせる</h3>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>画面端の場合</strong></span>は、派生のアロンから生DRで拾うところで、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「しゃがみ強 → 中タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 中）→ A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）or SA1 or SA3」のようにすることも可能</strong></span>で、これによってダメージを伸ばせます。</p>
<p>また、<strong><span style="color: #ff0000;">最大ダメージを目指したい場合は、「しゃがみ強 → キャンセルDR → 遅らせてしゃがみ強 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ ODダロイ（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ + 弱中 or 中強） → 派生のアロン → SA3」のようにすることも可能ですが、難易度が高い</span></strong>です。</p>
<p>&nbsp;</p>
<h4>・画面端の最大コンボ</h4>
<div class="supplement boader"><strong>ダメージ：6378</strong></p>
<p>ジャンプ強 or ジャンプA強（パニカン）→ しゃがみ強 → 強ダロイ（↓<span><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ </span>+ 強）→ 派生のアロン（→ + 弱 or 中 or 強）→ 生DR → しゃがみ強 → キャンセルDR → 遅らせてしゃがみ強 → 強タリム（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2199.png" alt="↙" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎← + 強）→ ODダロイ（↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" />→ + 弱中 or 中強） → 派生のアロン → CA</p>
</div>
<p>&nbsp;</p>
<h3>コンボの〆に飛び道具のパンギルを組み込む場合</h3>
<p>コンボの「強タリム → A強」の後の〆として、<strong><span style="color: #3366ff;">飛び道具のパンギル（↓↓ + 弱 or 中 or 強）を組み込み、飛び道具と挟み撃ちにする読み合いを仕掛けることも可能</span></strong>です。</p>
<p><strong><span style="color: #ff0000;">画面端などで密着状態でなければ、飛び道具を撃った後に少し動ける</span></strong>ため、微後ろ歩きしゃがみガードで<strong><span style="color: #3366ff;">シミーを狙うことも可能</span></strong>です。</p>
<p>そして<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>もし相手が裏周りしてくる飛び道具にヒットしたら「A強 → 強リパ（→↓<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2198.png" alt="↘" class="wp-smiley" style="height: 1em; max-height: 1em;" /> + 強）」などで追撃可能</strong></span>です。</p>
<p>&nbsp;</p>
<h3>SA2を利用するコンボについて</h3>
<p><strong><span style="color: #ff0000;">SA2を組み込むようなコンボもありますが、初心者が使いこなすにはゲージ管理なども含めて難易度が高くて難しい</span></strong>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>すぐに覚える必要はない</strong></span>です。</p>
<p>下手にSA2を使うより、SA1やSA3で〆るリーサルコンボを狙ったり、SA3を使える状態で立ち回って、SA3弾抜けでプレッシャーをかけたりする方が有効だと思います。</p>
<p>&nbsp;</p>
<h3>関連動画</h3>
<p><iframe loading="lazy" title="【スト6】モダンヤスミンの初心者向けコンボまとめ｜画面端の最大もできるぞ！" width="728" height="410" src="https://www.youtube.com/embed/M34jdkcDf3s?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe></p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はモダンヤスミンの初心者向けコンボについてご紹介しました。</p>
<p>ヤスミンについては<strong><span style="color: #ff0000;">バヤニ・モードにしてコンボを狙っていかないと火力が出せない</span></strong>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>初心者の方も含めて早めに今回ご紹介したようなコンボを覚えた方がよさそうなキャラ</strong></span>です。</p>
<p>また、ヤスミンはスタンダードキャラで使いやすいものの、格闘ゲームにおける立ち回り（地上戦など）や、読み合いをしっかりやらないと勝てないタイプのキャラだと思います。</p>
<p>初心者の方がヤスミンで勝つのは難しいかもしれませんが、これからヤスミンを使いたい方は、ぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>
<p style="text-align: center;"><strong>＼ よければXのフォロー・YouTubeのチャンネル登録もぜひお願いします！ ／</strong></p>
<div class="supplement boader">
<p>・Xのフォローは<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener">こちら</a></p>
<p>・YouTubeのチャンネル登録は<a href="https://www.youtube.com/channel/UCehXknUVdKmYct3r_ecqwLw?sub_confirmation=1" target="_blank" rel="noopener">こちら</a></p>
</div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/sf6-modern-yasmin-combo">【スト6】モダンヤスミンの初心者向けコンボまとめ｜簡単で実戦向き</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/sf6-modern-yasmin-combo/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Substackを始めました！注目のニュースレター（メルマガ）＆SNSサービス</title>
		<link>https://tomoyuki65.com/substack</link>
					<comments>https://tomoyuki65.com/substack#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Thu, 02 Jul 2026 08:43:42 +0000</pubDate>
				<category><![CDATA[1. お知らせ]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20546</guid>

					<description><![CDATA[<p>  こんにちは。Tomoyuki（@tomoyuki65）です。 最近はどのSNSもアルゴリズム（どの投稿を誰に表示するかを決める仕組み）中心のタイムライン（投稿が流れてくる画面）になっていて、フォローしている人の投稿が...</p>
The post <a href="https://tomoyuki65.com/substack">Substackを始めました！注目のニュースレター（メルマガ）＆SNSサービス</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/07/substack-start-3.jpg" alt="" width="672" height="480" class="aligncenter size-full wp-image-20549" srcset="https://tomoyuki65.com/wp-content/uploads/2026/07/substack-start-3.jpg 672w, https://tomoyuki65.com/wp-content/uploads/2026/07/substack-start-3-300x214.jpg 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p><span style="font-size: 16.48px; letter-spacing: 0.1em;"> </span></p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>最近は<strong></strong><span style="background-color: #ffc6d1;"><strong>どのSNSもアルゴリズム（どの投稿を誰に表示するかを決める仕組み）中心のタイムライン（投稿が流れてくる画面）</strong></span>になっていて、<strong><span style="color: #ff0000;">フォローしている人の投稿が見つけにくく、逆にフォローしてくれている人にも情報が届きにくい状態</span></strong>になっています。</p>
<p>そんな中、最近注目され始めているのが<strong>『<span style="border-bottom: 2px solid #be3144;">Substack</span>』</strong>です。</p>
<p>私も気になったので実際に使ってみたところ、思っていた以上に使い勝手がよく、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>これから継続的にSubstackでも情報発信</strong></span>をしていこうと思っています。</p>
<p>&nbsp;</p>
<h2>Substack（サブスタック）とは？</h2>
<p>Substack（サブスタック）は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ニュースレター（メールマガジン）の配信を中心に、記事や音声、動画などのコンテンツを発信できるプラットフォーム</strong></span>です。</p>
<p>メールで読者に直接コンテンツを届けられるだけでなく、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>X（旧Twitter）のようなタイムライン機能「Notes」</strong></span>も備えており、気軽な投稿や交流も楽しめます。</p>
<p>また、Xのようにおすすめ投稿が次々と表示されるアルゴリズムが中心ではないため、<span style="color: #3366ff;"><strong>自分が購読・フォローした情報だけをチェックすることが可能</strong></span>です。</p>
<p>興味のない情報に振り回されたくない人にとっては、使いやすいサービスになっています。</p>
<p>&nbsp;</p>
<h2>私が実際に使ってみた感想</h2>
<p>私はまだ使い始めたばかりではありますが、やはり<span style="color: #3366ff;"><strong>一番魅力的に感じた部分</strong></span>については、<span style="color: #3366ff;"><strong>ニュースレター（メルマガ）に登録してくれた方へ直接情報が届けられること</strong></span>です。</p>
<p>というのも、以前からメルマガには興味があったものの、始めるハードルが高いなと感じており、これまで一切やるつもりはありませんでした。</p>
<p>しかし、Substackなら<span style="color: #3366ff;"><strong>パスワードレスかつメールアドレスだけでサクッとアカウントが作れる点や、XのようなSNS機能として使い始められる点が非常に良かった</strong></span>です。</p>
<p>また、タイムラインに余計な情報が流れてこないため、必要な情報を追いやすいのも気に入っています。</p>
<p>&nbsp;</p>
<h2>ITエンジニア向けの情報発信に利用します！</h2>
<p>今後は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>主にITエンジニア向けの情報発信</strong></span>を目的として、Substackを活用していこうと思っています。</p>
<p>特に<strong></strong><span style="background-color: #ffc6d1;"><strong>ニュースレター（メルマガ）は不定期</strong></span>ですが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ブログや他のSNSでは書きにくい内容をいい感じにまとめて発信していく予定</strong></span>です。</p>
<p>ITエンジニアの方や、技術に興味がある方に向けて役立つ情報をお届けできればと思っているので、興味のある方はぜひニュースレター（メルマガ）に登録していただけると嬉しいです。</p>
<p>&nbsp;</p>
<h2>最後に</h2>
<p>今回はSubstackを始めたことをお知らせしました。</p>
<p>実際に使ってみたら思いのほか使いやすく、これからも継続的に利用していけそうだなと感じています。</p>
<p>興味を持っていただけた方は、ぜひ私の<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ニュースレター（メルマガ）に登録</strong></span>をお願いします！</p>
<p>&nbsp;</p>
<div style="text-align: center;"><iframe loading="lazy" width="480" height="320" style="border: 1px solid #EEE; background: white;" src="https://tomoyuki65.substack.com/embed" frameborder="0" scrolling="no"></iframe></div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/substack">Substackを始めました！注目のニュースレター（メルマガ）＆SNSサービス</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/substack/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Next.js v16でAI駆動開発を実践！モダンフロントエンド開発におけるSDD・TDD・Codex・ハーネスエンジニアリング</title>
		<link>https://tomoyuki65.com/nextjs-v16-ai-driven-frontend-development</link>
					<comments>https://tomoyuki65.com/nextjs-v16-ai-driven-frontend-development#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Fri, 08 May 2026 14:33:21 +0000</pubDate>
				<category><![CDATA[4. プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20473</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 2026年以降のシステム開発では、コードはAIにかかせる「AI駆動開発」が主流になるのは避けられません。 特にフロントエンド開発については、バッ...</p>
The post <a href="https://tomoyuki65.com/nextjs-v16-ai-driven-frontend-development">Next.js v16でAI駆動開発を実践！モダンフロントエンド開発におけるSDD・TDD・Codex・ハーネスエンジニアリング</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-11.jpg" alt="" width="672" height="480" class="aligncenter size-full wp-image-20532" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-11.jpg 672w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-11-300x214.jpg 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>2026年以降のシステム開発では、コードはAIにかかせる<strong>「<span style="border-bottom: 2px solid #be3144;">AI駆動開発</span>」</strong>が主流になるのは避けられません。</p>
<p>特にフロントエンド開発については、バックエンド開発に比べて責任範囲が狭く、AIが生成したコードをそのまま採用しても致命的な障害に繋がりにくいため、<strong><span style="color: #ff0000;">モダンなフロントエンド開発はどんどんAI駆動開発に置き換わっていきそう</span></strong>です。</p>
<p>そんな私も<strong><span style="color: #3366ff;">直近ではRuby on RailsによるAI駆動開発を試してAIの凄さを体感</span></strong>し、<strong><span style="border-bottom: 2px solid #be3144;">次はフロントエンド開発にも適用していくべきと思い、Next.jsでも試してみることにしました。</span></strong></p>
<p>ということでこの記事では、私が試したNext.jsによるAI駆動開発の実践方法についてご紹介します。</p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/rails-ai-tdd-ddd-modular-monolith"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-0-486x290.jpg" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">Ruby on Railsで始めるAI駆動開発×TDD実践｜Codexとハーネスエンジニアリングで作るDDDモジュラーモノリス</p><span class="date gf">2026年4月24日</span></div></a></div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2><span>Next.js v16でAI駆動開発を実践！モダンフロントエンド開発におけるSDD・TDD・Codex・ハーネスエンジニアリング</span></h2>
<p>まず今回はmacOS（Apple silicon）のPCを使い、パッケージ管理に「<a href="https://brew.sh/ja/" target="_blank" rel="noopener">Homebrew</a>」、コード管理に「Git」や「<a href="https://github.com" target="_blank" rel="noopener">GitHub</a>」、AIツールはOpenAIの「<a href="https://openai.com/ja-JP/codex/" target="_blank" rel="noopener">Codex</a>」を利用する前提で進めます。</p>
<p>以降の内容を参考にしたい場合は、事前にこれらを利用できるように準備して下さい。</p>
<p><span style="color: #ff0000;">※GitHub CLIとして「gh」コマンドも準備が必要です。これはHomebrewでインストールできます。</span></p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/openai-codex-cli-app-guide" target="_blank" rel="noopener">・OpenAI Codex CLI / Codexアプリの使い方【ChatGPT時代のAI開発ツール入門】</a></p>
<p>&nbsp;</p>
<h3>Node.jsの準備</h3>
<p>まず<span style="color: #ff0000;"><strong>フロントエンド開発ではNode.jsを使う必要</strong></span>があり、その際には<strong></strong><span style="background-color: #ffc6d1;"><strong>何らかのバージョン管理ツールを利用するのがおすすめ</strong></span>です。</p>
<p>私は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>volta</strong></span>を気に入っているので、今回はその例でご紹介しますが、voltaをインストールしたい場合は以下のcurlコマンドを実行すると可能です。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ curl https://get.volta.sh | bash</code></pre>
</div>
<p>&nbsp;</p>
<p>インストール後、以下のコマンドでパスが通っているか確認して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ volta -v</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、nodeをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ volta install node@24.15.0</code></pre>
</div>
<p><span style="color: #ff0000;">※ここでは2026年5月時点でLTS版のバージョン「24.15.0」を使います。尚、チーム開発などではnodeのバージョンは固定推薦です。</span></p>
<p>&nbsp;</p>
<p>インストール後、以下のコマンドでパスが通っているか確認して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ node -v</code></pre>
</div>
<p>&nbsp;</p>
<h3>パッケージマネージャー「pnpm」の準備</h3>
<p>次にnodeを使う際はパッケージマネージャーとして付属のnpmを使うことが多いですが、最近だとCI（継続的インテグレーション）でキャッシュを利用することを考慮し、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>pnpmの利用が増えている</strong></span>ため、今回はそれを試します。</p>
<p><del>そんなpnpmもバージョン固定を想定し、<strong></strong><span style="background-color: #ffc6d1;"><strong>corepackを利用する方法で導入</strong></span>するため、まずは以下のコマンドを実行してcorepackをインストールします。</del></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><del><code>$ volta install corepack</code></del></pre>
</div>
<p>&nbsp;</p>
<p><del>インストール後、以下のコマンドでパスが通っているか確認して下さい。</del></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><del><code>$ corepack -v</code></del></pre>
</div>
<p>&nbsp;</p>
<p><del>次に以下のコマンドを実行し、corepackを有効化します。</del></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><del><code>$ corepack enable</code></del></pre>
</div>
<p>&nbsp;</p>
<p><del>次に以下のコマンドを実行します。</del></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><del><code>$ pnpm -v</code></del></pre>
</div>
<p>&nbsp;</p>
<p><del>コマンドを実行すると<span style="color: #ff0000;">pnpmをインストールするか聞かれる</span>ので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「y」を入力</strong></span>してインストールを進めます。</del></p>
<p><del>インストール完了後、再度以下のコマンドを実行し、パスが通っていればOKです。</del></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><del><code>$ pnpm -v</code></del></pre>
</div>
<p><del><span style="color: #ff0000;">※今回の最新バージョンは「10.33.2」でした。</span></del></p>
<p>&nbsp;</p>
<p>今後はcorepackは非推薦になっていくようなので、pnpmのインストールに関しては、普通にHomebrew経由でインストールすればよさそうです。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ brew install pnpm</code></pre>
</div>
<p>&nbsp;</p>
<h3>Next.jsの新規プロジェクト作成</h3>
<p>次に以下のコマンドを実行し、Next.jsの新規プロジェクトを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir next16-aidd &amp;&amp; cd next16-aidd
$ pnpm create next-app@latest .</code></pre>
</div>
<p><span style="color: #ff0000;">※今回は2026年5月時点で最新のバージョン16を利用しています。</span></p>
<p>&nbsp;</p>
<p>コマンドを実行後、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「No, customize settings」を選択</strong></span>し、以下の選択肢で作成します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-1.jpg" alt="" width="1304" height="430" class="aligncenter size-full wp-image-20480" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-1.jpg 1304w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-1-300x99.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-1-1024x338.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-1-768x253.jpg 768w" sizes="auto, (max-width: 1304px) 100vw, 1304px" />
<p>&nbsp;</p>
<div class="supplement boader">
<ul>
<li> TypeScript を使用しますか？：「Yes」</li>
<li> どのリンターを使用しますか？：「Biome」</li>
<li> React Compiler を使用しますか？：「No」</li>
<li> Tailwind CSS を使用しますか？：「Yes」</li>
<li> コードを src/ ディレクトリ内に配置しますか？：「Yes」</li>
<li> App Router を使用しますか？（推奨）：「Yes」</li>
<li> インポートエイリアス（デフォルトでは @/*）をカスタマイズしますか？：「No」</li>
<li> 最新の Next.js コードを書くためのガイドとして AGENTS.md を含めますか？：「No」</li>
</ul>
</div>
<p><span style="color: #ff0000;">※フォーマッターやlintにはモダンな「Biome」を試します。</span></p>
<p>&nbsp;</p>
<p>実行後、以下のようにNext.jsのプロジェクトが作成されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-2.png" alt="" width="1344" height="822" class="aligncenter size-full wp-image-20482" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-2.png 1344w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-2-300x183.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-2-1024x626.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-2-768x470.png 768w" sizes="auto, (max-width: 1344px) 100vw, 1344px" />
<p>&nbsp;</p>
<p>次にローカルサーバーの起動を試すには以下のコマンドを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm dev</code></pre>
</div>
<p>&nbsp;</p>
<p>サーバー起動後、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ブラウザで「http://localhost:3000」にアクセス</strong></span>し、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-3.png" alt="" width="2558" height="1824" class="aligncenter size-full wp-image-20483" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-3.png 2558w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-3-300x214.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-3-1024x730.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-3-768x548.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-3-1536x1095.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-3-2048x1460.png 2048w" sizes="auto, (max-width: 2558px) 100vw, 2558px" />
<p>&nbsp;</p>
<p><strong><span style="color: #ff0000;">起動したサーバーを止めたい場合</span></strong>は、ターミナルがアクティブの状態でショートカットキー<span style="color: #ff0000;"><strong>「control + c」</strong></span>を使うと止めれます。</p>
<p>&nbsp;</p>
<h3>nodeとpnpmのバージョン固定</h3>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>nodeとpnpmのバージョン固定</strong></span>を行います。まずは以下のコマンドを実行し、ファイル「.npmrc」を作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch .npmrc</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイル「.npmrc」は以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".npmrc"><code>engine-strict=true</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「package.json」に対して以下のように<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「engines」、「packageManager」を追加</strong></span>します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="package.json"><code>{
  "name": "next16-aidd",
  "version": "0.1.0",
  "private": true,
  "engines": {
    "node": "24.15.0",
    "pnpm": "10.33.2"
  },
  "packageManager": "pnpm@10.33.2",
  "scripts": {

・・・</code></pre>
</div>
<p><span style="color: #ff0000;">※pnpmのバージョンはインストール時に「10.33.2」だったのでこれで固定しています。必要に応じて修正して下さい。</span></p>
<p>&nbsp;</p>
<p><del>次に以下のコマンドを実行し、corepackでpnpmを固定できるようにします。</del></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><del><code>$ corepack use pnpm@10.33.2</code></del></pre>
</div>
<p><del><span style="color: #ff0000;">※pnpmのバージョンはインストール時に「10.33.2」だったのでこれで固定しています。必要に応じて修正して下さい。</span></del></p>
<p><del>コマンド実行後、「package.json」に「packageManager」が追加されればOKです。</del></p>
<p><span style="color: #ff0000;">※corepackは利用しないように修正しています。</span></p>
<p>&nbsp;</p>
<h3>biomeのコマンド修正</h3>
<p>次にファイル「package.json」にあるbiomeのコマンドを以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="package.json"><code>・・・

  "scripts": {
    
    ・・・

    "lint": "biome lint",
    "format": "biome format --write",
    "check": "biome check --write",
    
    ・・・
  },

・・・</code></pre>
</div>
<p><span style="color: #ff0000;">※基本的に「check」（フォーマット修正、import整理、lintチェック）を実行するのがおすすめです。</span></p>
<p>&nbsp;</p>
<h3>pnpmのストアをgit管理対象外にする</h3>
<p>次にpnpmを利用した際に作成されるストア（共有キャッシュ）ファイルをgit管理の対象から外すため、「.gitignore」に以下を追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".gitignore"><code>・・・

# pnpm
.pnpm-store</code></pre>
</div>
<p>&nbsp;</p>
<h3>Storybookの導入</h3>
<p>次にフロントエンド開発ではSDD（Storybook Driven Development）で行うのが最適なので、以下のコマンドを実行してStorybookを導入します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm create storybook@latest</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、オンボーディングの有無が聞かれるので、<strong><span style="border-bottom: 2px solid #be3144;">どちらかを選択</span></strong>します。</p>
<div class="supplement boader">
<ul>
<li> Yes（はい）: Help me with onboarding（オンボーディングを手伝ってください）</li>
<li> No（いいえ）: Skip onboarding &amp; don&#8217;t ask again（オンボーディングをスキップして、今後表示しないでください）</li>
</ul>
</div>
<p>&nbsp;</p>
<p>次に<strong>「Do you want to install Playwright with Chromium now?（PlaywrightをChromiumと一緒にインストールしますか？）」</strong>も聞かれるので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「Yes」を選択</strong></span>します。</p>
<p><span style="color: #ff0000;">※Playwright（プレイライト）は、Microsoft製のWebブラウザ操作を自動化するOSSのフレームワークです。Chromium、Firefox、WebKit（Safari）の3つの主要エンジンを1つのAPIで操作でき、高速かつ安定したE2Eテストやスクレイピング、業務自動化に利用されています。</span></p>
<p>&nbsp;</p>
<p>全て完了後、ブラウザで「http://localhost:6006」が自動で開き、Storybookの画面が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-4.jpg" alt="" width="2560" height="1834" class="aligncenter size-full wp-image-20486" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-4.jpg 2560w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-4-300x215.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-4-1024x734.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-4-768x550.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-4-1536x1100.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-4-2048x1467.jpg 2048w" sizes="auto, (max-width: 2560px) 100vw, 2560px" />
<p><span style="color: #ff0000;">※Storybookのサーバーも止めたい場合はターミナルがアクティブの状態でショートカットキー「control + c」を使います。</span></p>
<p>&nbsp;</p>
<p>次に<strong><span style="background-color: #ffc6d1;">StorybookでTailwindCSSを有効化するのと、viewportの設定も追加しておく</span></strong>ため、ファイル「.storybook/preview.ts」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file=".storybook/preview.ts"><code>import type { Preview } from '@storybook/nextjs-vite'
// TailwindCSSの読み込み
import "@/app/globals.css";
// viewportをインポート
import { INITIAL_VIEWPORTS, MINIMAL_VIEWPORTS } from "storybook/viewport";

const preview: Preview = {
  parameters: {
    // viewportの設定追加
    viewport: {
      options: {
        ...INITIAL_VIEWPORTS,
        ...MINIMAL_VIEWPORTS,
      },
    },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },

    a11y: {
      // 'todo' - show a11y violations in the test UI only
      // 'error' - fail CI on a11y violations
      // 'off' - skip a11y checks entirely
      test: 'todo'
    }
  },
};

export default preview;</code></pre>
</div>
<p>&nbsp;</p>
<p>次にNext.jsで合わせて導入してある<strong><span style="color: #ff0000;">Biome（format・lintツール）のチェック</span></strong>で、<strong><span style="color: #ff0000;">Storybookのインストール時に自動で追加されたファイルをチェックの対象から外す</span></strong>ため、ファイル「biome.json」の「files」に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「&#8221;!src/stories&#8221;」を追加</strong></span>します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="biome.json">{

・・・
<code>
  "files": {
    "ignoreUnknown": true,
    "includes": [
      "**",
      "!node_modules",
      "!.next",
      "!dist",
      "!build",
      "!src/stories"
    ]
  },

・・・</code></pre>
</div>
<p>&nbsp;</p>
<h3>vitestの設定</h3>
<p>Storybookを導入すると、合わせてviteやvitestも導入され、特にvitestはフロントエンド開発のunitテストやintegrationテストで利用します。</p>
<p>そのままだと<strong><span style="color: #ff0000;">必要なライブラリが足りない</span></strong>ため、以下のコマンドを実行して<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>追加のライブラリをインストール</strong></span>します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add -D jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @vitejs/plugin-react msw msw-storybook-addon @playwright/test</code></pre>
</div>
<p>&nbsp;</p>
<p>次に一部のライブラリは導入に承認が必要になるため、以下のコマンドを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm approve-builds</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、 Choose which packages to build（ビルドするパッケージを選択してください）が聞かれるので、そのままEnterを押して実行すると、承認完了です。</p>
<p><span style="color: #ff0000;">※この承認作業については、よくわからないライブラリは入れないようにするためのもの（セキュリティ対策）なので、その点は理解して承認して下さい。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、vitest用のセットアップファイルを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch vitest.setup.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイル「vitest.setup.ts」を以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="vitest.setup.ts"><code>import "@testing-library/jest-dom";</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「vitest.config.ts」を以下のように修正し、ユニットテスト用の設定を追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="vitest.config.ts"><code>・・・

// vitest用の設定追加
import react from "@vitejs/plugin-react";

const dirname =
  typeof __dirname !== 'undefined'
  ? __dirname
  : path.dirname(fileURLToPath(import.meta.url));

// エイリアス設定追加
const alias = [
  {
    find: /^@\//,
    replacement: `${path.join(dirname, "src")}/`,
  },
  {
    find: "@",
    replacement: path.join(dirname, "src"),
  },
] as const;

// More info at: https://storybook.js.org/docs/next/writing-tests/integrations/vitest-addon
export default defineConfig({
  // エイリアス設定追加
  resolve: {
    alias,
  },
  test: {
    projects: [
      // ユニットテスト用（integration含む）
      {
        plugins: [react()],
        resolve: {
          alias,
        },
        test: {
          name: "unit",
          environment: "jsdom",
          globals: true,
          setupFiles: ["./vitest.setup.ts"],
          include: [
            "src/**/*.{test,spec}.{ts,tsx}",
            "tests/integration/**/*.{test,spec}.{ts,tsx}",
          ],
        },
      },
      // Storybook用
      {

・・・</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「package.json」にテスト用コマンドを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="package.json"><code>・・・

  "scripts": {
    
    ・・・

    "build-storybook": "storybook build", // 末尾に追加するならカンマ追加して下さい
    "test": "vitest run --project unit --reporter=verbose",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui"
  },

・・・</code></pre>
</div>
<p><span style="color: #ff0000;">※playwright用のコマンドも合わせて追加してます。</span></p>
<p>&nbsp;</p>
<h3>mswの設定</h3>
<p>次にmsw（APIのモック用サーバーライブラリ）の設定を行います。</p>
<p>まずは以下のコマンドを実行し、各種ファイルを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/lib/msw/handlers &amp;&amp; touch src/lib/msw/handlers/sample.ts
$ mkdir -p src/lib/msw/setup &amp;&amp; touch src/lib/msw/setup/server.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/lib/msw/handlers/sample.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/lib/msw/handlers/sample.ts"><code>import { HttpResponse, http, type RequestHandler } from "msw";

const helloHandler = http.get("http://localhost:3000/hello", () =&gt; {
  return HttpResponse.json({
    message: "Hello World !!",
  });
});

export const sampleHandlers: RequestHandler[] = [helloHandler];</code></pre>
</div>
<p><span style="color: #ff0000;">※これはモック化用のハンドラーファイルのサンプルです。</span></p>
<p>&nbsp;</p>
<p>・「src/lib/msw/setup/server.ts」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="src/lib/msw/setup/server.ts"><code>import { setupServer } from "msw/node";
import { sampleHandlers } from "../handlers/sample";

// mswの設定
export const server = setupServer(...sampleHandlers);</code></pre>
</div>
<p><span style="color: #ff0000;">※ハンドラーファイルを追加した際はsetupServerに追加して下さい。</span></p>
<p>&nbsp;</p>
<p>次にファイル「vitest.setup.ts」にmswの設定を追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="vitest.setup.ts"><code>import "@testing-library/jest-dom";
// mswの設定追加
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./src/lib/msw/setup/server";

beforeAll(() =&gt; server.listen());
afterEach(() =&gt; server.resetHandlers());
afterAll(() =&gt; server.close());</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Storybookなどのブラウザ用の設定ファイルを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm exec msw init public --save</code></pre>
</div>
<p><span style="color: #ff0000;">※実行後、ファイル「public/mockServiceWorker.js」が追加されます。</span></p>
<p>&nbsp;</p>
<p>次に作成されたファイルをBiome（format・lintツール）のチェック対象から外すため、ファイル「biome.json」の「files」に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「&#8221;!public/mockServiceWorker.js&#8221;」を追加</strong></span>します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-json" data-lang="JSON" data-file="biome.json">{

・・・
<code>
  "files": {
    "ignoreUnknown": true,
    "includes": [

      ・・・

      "!src/stories",
      "!public/mockServiceWorker.js"
    ]
  },

・・・</code></pre>
</div>
<p>&nbsp;</p>
<p>次に<strong></strong><span style="background-color: #ffc6d1;"><strong>Storybookでmswを使えるようにする</strong></span>ため、ファイル「.storybook/preview.ts」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file=".storybook/preview.ts"><code>・・・

// mswの設定追加
import { initialize, mswLoader } from "msw-storybook-addon";
import { sampleHandlers } from "@/lib/msw/handlers/sample";

// mswの初期化
initialize({ onUnhandledRequest: "warn" }, [
  ...sampleHandlers,
]);

const preview: Preview = {
  // mswの設定追加
  loaders: [mswLoader],

・・・

};

export default preview;</code></pre>
</div>
<p><span style="color: #ff0000;">※ハンドラーファイルを追加した際はinitializeに追加して下さい。</span></p>
<p>&nbsp;</p>
<h3>playwrightの設定</h3>
<p>次にplaywrightの設定をしていきます。</p>
<p>まずは以下のコマンドを実行し、設定ファイルを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch playwright.config.ts</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイル「playwright.config.ts」を以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ts" data-lang="TypeScript" data-file="playwright.config.ts"><code>import { defineConfig } from "@playwright/test";

export default defineConfig({
  testDir: "./tests/e2e",

  use: {
    baseURL: "http://localhost:3000",
    headless: true,
  },

  webServer: {
    command: "pnpm dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});</code></pre>
</div>
<p>&nbsp;</p>
<p>次に<span style="color: #ff0000;"><strong>playwrightを実行した際に作成されるファイルをgitの管理対象外にする</strong></span>ため、ファイル「.gitignore」に以下を追加しておきます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".gitignore"><code>・・・

# playwright
test-results/
playwright-report/</code></pre>
</div>
<p>&nbsp;</p>
<h3>lefthookの導入（任意）</h3>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>チーム開発向けにbiomeのチェックやテストを自動化</strong></span>するため、Gitフック管理ツールを導入します。</p>
<p>フロントエンド開発ではよくhuskyなどが使われますが、今回は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Go言語製で他のプログラミング言語でも使いやすい「lefthook」</strong></span>を使います。</p>
<p>まずは以下のコマンドを実行し、lefthookをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add -D lefthook</code></pre>
</div>
<p>&nbsp;</p>
<p>次に承認が必要になるため、以下のコマンドを実行して承認して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm approve-builds</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、lefthookを初期化します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm exec lefthook install</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成されたファイル「lefthook.yml」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="lefthook.yml"><code>pre-commit:
  parallel: true
  commands:
    biome:
      run: pnpm check

pre-push:
  parallel: false
  commands:
    unit-test:
      run: pnpm test
    e2e-test:
      run: pnpm test:e2e</code></pre>
</div>
<p>&nbsp;</p>
<h3>各種コマンド一覧</h3>
<p>・Storybookの起動（UIカタログ）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>pnpm storybook</code></pre>
</div>
<p>&nbsp;</p>
<p>・ローカルサーバー起動（Next.jsアプリ）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>pnpm dev</code></pre>
</div>
<p>&nbsp;</p>
<p>・biome check（フォーマット修正・import整理・lint）実行</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>pnpm check</code></pre>
</div>
<p>&nbsp;</p>
<p>・テスト「unit/integration」実行</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>pnpm test</code></pre>
</div>
<p>&nbsp;</p>
<p>・テスト「e2e」実行（UIなし）</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>pnpm test:e2e</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>・テスト「e2e」実行（UIあり）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>pnpm test:e2e:ui</code></pre>
</div>
<p><span style="color: #ff0000;">※もしplaywrightのブラウザのインストールが必要なら「pnpm exec playwright install」を実行して下さい。</span></p>
<p>&nbsp;</p>
<h3>その他のライブラリについて</h3>
<p>その他、<strong><span style="color: #ff0000;">必要に応じて以下のようなライブラも追加</span></strong>して下さい。</p>
<p>・フォーム用ライブラリ「react-hook-form」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add react-hook-form @hookform/resolvers</code></pre>
</div>
<p>&nbsp;</p>
<p>・バリデーション用ライブラリ「zod」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add zod</code></pre>
</div>
<p>&nbsp;</p>
<p>・React向けのデータ取得・キャッシュ管理ライブラリ「swr」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm add swr</code></pre>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>OpenAI「Codex」におけるハーネスエンジニアリング設定</h2>
<p>AI駆動開発をする際には何らかのAIツールを利用しますが、<span style="color: #ff0000;"><strong>今回はOpenAIの「Codexアプリ」または「Codex CLI」を利用する前提</strong></span>としています。</p>
<p>そしてAIツールを上手く使って開発するためには<strong><span style="background-color: #ffc6d1;">『ハーネスエンジニアリング』が重要</span></strong>になっており、<strong><span style="color: #ff0000;">事前にしっかりハーネス設計をする必要</span></strong>があります。</p>
<p>&nbsp;</p>
<h3>ハーネスエンジニアリングとは？</h3>
<p>AIツールにおける『ハーネスエンジニアリング』とは、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>AIモデルやAIエージェントを安全かつ安定して動作させるため、入力・出力の制御やルール設定、評価・テストの仕組みなどを設計・構築する技術</strong></span>のことです。</p>
<p>これによってAIの挙動を管理し、<span style="color: #3366ff;"><strong>品質や再現性を担保した状態で実運用</strong></span>できるようにします。</p>
<p>&nbsp;</p>
<h3>OpenAI「Codex」におけるハーネス設計について</h3>
<p>OpenAI「Codex」でハーネス設計をしたい場合は、以下のようなディレクトリ構成で各種ファイルを作成し、それぞれ内容を定義していくことになります。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>/my-project
 ├── /.codex
 |    ├── config.toml  ※codexに関する設定
 |    |
 |    ├── /rules  ※sandbox外で実行できるコマンド制御の設定
 |    |    └── default.rules
 |    |
 |    ├── /agents  ※サブエージェントを利用する場合の例
 |    |    ├── orchestrator.toml  ※指揮者（全体制御）
 |    |    ├── tester.toml        ※テスター（テストコード作成・検証）
 |    |    ├── implementer.toml   ※実装者（テストコードを通すように実装）
 |    |    └── reviewer.toml      ※レビュワー（設計や品質チェック）
 |    |
 |    ├── /workflows  ※ワークフローを利用する場合の例
 |    |    └── sdd_flow.md  ※SDD（Storybook Driven Development）のフロー
 |    |
 |    └── /skills  ※Agent Skillsを利用する場合
 |
 ├── AGENTS.md  ※ルートディレクトリ用（共通ルール）
 |
 ├── /src
 |    └── AGENTS.md  ※サブディレクトリ用（専用ルール）
 |
 └── /docs  ※詳細仕様にファイル分割して格納（各種AGENTS.mdから指定する）
      └── /rules
           ├── architecture.md  ※設計思想のルール定義
           ├── database.md      ※DB設計のルール定義
           ├── sub.md           ※サブディレクトリ用のルール定義
           └── testing.md       ※テスト用のルール定義
 </code></pre>
</div>
<p>&nbsp;</p>
<h3>codexの設定ファイルを作成</h3>
<p>まずはcodexの設定ファイルを作成するため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir .codex &amp;&amp; touch .codex/config.toml</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「.codex/config.toml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".codex/config.toml"><code>model = "gpt-5.2"
model_reasoning_effort = "medium"

sandbox_mode = "workspace-write"</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はモデルに「gpt-5.2」を指定（利用中のプランで使用可能なものを指定して下さい）、モデルの推論設定に「medium（バランス型）」を指定、サンドボックスモードに「workspace-write（書き込み許可）」を指定</span></p>
<p>&nbsp;</p>
<p>尚、<strong><span style="color: #ff0000;">Codexアプリなどで対象のプロジェクトに設定した「.codex/config.toml」を有効化するには、対象のプロジェクトを信頼するプロジェクトにする必要</span></strong>があります。</p>
<p>対象のプロジェクトを信頼するプロジェクトにしたい場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>グローバル設定の方のconfig.toml（~/.codex/config.toml）で、以下のような設定を追加</strong></span>して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="~/.codex/config.toml"><code>[projects."対象のプロジェクトのフルパス"]
trust_level = "trusted"</code></pre>
</div>
<p><span style="color: #ff0000;">※設定変更を反映するにはCodexアプリの再起動が必要です。</span></p>
<p>&nbsp;</p>
<h3>sandbox外のコマンド制御設定を追加</h3>
<p>次にsandbox外で実行できるコマンドを制御するため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/rules &amp;&amp; touch .codex/rules/default.rules</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「.codex/rules/default.rules」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".codex/rules/default.rules"><code># --- ディスク破壊（即禁止） ---
prefix_rule(
    pattern=["mkfs", "dd if=", "wipefs"],
    decision="forbidden",
    justification="ストレージ破壊操作"
)

# --- Git履歴破壊（即禁止） ---
prefix_rule(
    pattern=["git push --force", "git push -f", "git push --force-with-lease"],
    decision="forbidden",
    justification="履歴改変（強制push）によるリポジトリ破壊の可能性"
)

# --- GitHub CLI 危険操作 ---
prefix_rule(
    pattern=["gh repo delete"],
    decision="forbidden",
    justification="リポジトリ削除（不可逆）"
)

prefix_rule(
    pattern=["gh pr merge --admin"],
    decision="prompt",
    justification="保護ルール無視の強制マージ"
)

prefix_rule(
    pattern=["gh workflow run"],
    decision="prompt",
    justification="CI/CDの強制実行"
)

prefix_rule(
    pattern=["gh secret set", "gh secret delete"],
    decision="prompt",
    justification="機密情報の変更"
)
# --- 危険削除（確認） ---
prefix_rule(
    pattern=["rm -rf", "sudo rm"],
    decision="prompt",
    justification="不可逆削除の可能性"
)

# --- 権限昇格 ---
prefix_rule(
    pattern=["sudo"],
    decision="prompt",
    justification="システム影響が大きい"
)</code></pre>
</div>
<p><span style="color: #ff0000;">※まずは危険なコマンドを制御する</span></p>
<p>&nbsp;</p>
<h3>agents（サブエージェント）機能を追加</h3>
<p>次にagents（サブエージェント）機能を試すため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/agents
$ touch .codex/agents/pm.toml .codex/agents/tester.toml .codex/agents/implementer.toml .codex/agents/reviewer.toml</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はpm（プロダクトマネージャー・指揮者）、tester（テスター）、implementer（実装者）、reviewer（レビュワー）の4名を想定しています。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「.codex/agents/pm.toml」（プロダクトマネージャー / 指揮者）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/agents/pm.toml"><code>name = "pm"
description = "全体の進行管理とタスク分解を行うプロダクトマネージャー"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたは開発全体を指揮するプロダクトマネージャーです。

役割：
- 要件を整理し、タスクに分解する
- tester → implementer → reviewer の順でタスクを割り振る
- 各エージェントのアウトプットを評価し、次の行動を決める

ルール：
- 必ずTDDサイクル（RED → GREEN → REVIEW）を守る
- 不完全な実装は次に進めない
- 問題があれば前の工程に差し戻す

出力形式：
- 次に行動するエージェント名
- 依頼内容（具体的に）
"""</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/agents/tester.toml」（テスター）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/agents/tester.toml"><code>name = "tester"
description = "テストコード作成と動作検証を行うTDDエンジニア"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたはTDDに従うテストエンジニアです。

役割：
- 仕様に基づいたテストコードを書く（RED）
- 実装後にテストを実行し検証する（GREEN）

ルール：
- まず失敗するテストを書く（RED）
- テストは仕様を正確に表現すること
- エッジケースも考慮する
- 実装コードは書かない

出力：
- テストコード
- テストの意図説明
"""</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/agents/implementer.toml」（実装者）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/agents/implementer.toml"><code>name = "implementer"
description = "テストを通すための実装を行うエンジニア"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたはTDDに従う実装エンジニアです。

役割：
- テストが通る最小限の実装を書く

ルール：
- テストを通すことを最優先
- 過剰な設計をしない（YAGNI）
- テストを書かない
- リファクタは必要最低限

出力：
- 実装コード
- 実装の簡単な説明
"""</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/agents/reviewer.toml」（レビュワー）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/agents/reviewer.toml"><code>name = "reviewer"
description = "仕様適合性・品質・セキュリティをレビューするエンジニア"
model_reasoning_effort = "high"
sandbox_mode = "read-only"

developer_instructions = """
あなたはコードレビュー担当です。

役割：
- 実装が仕様を満たしているか確認
- バグ・セキュリティ問題を検出
- テスト不足を指摘

観点：
- 正しさ（仕様通りか）
- セキュリティ
- 境界値・異常系
- テスト網羅性

ルール：
- 指摘は具体的に
- 再現手順を書く
- スタイル指摘は重要な場合のみ

出力：
- 指摘一覧（重要度付き）
- 修正提案
"""</code></pre>
</div>
<p>&nbsp;</p>
<h3>workflows（ワークフロー）機能を追加</h3>
<p>次にworkflows（ワークフロー）機能も試すため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/workflows &amp;&amp; touch .codex/workflows/sdd_flow.md</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はSDD開発を前提とします。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「.codex/workflows/sdd_flow.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/workflows/sdd_flow.md"><code># SDD開発フロー（Storybook Driven Development）

フロントエンド開発における Storybook Driven Development（SDD）の標準フローを定義する。

- stories は UI仕様書として扱う。
- tests は必要に応じた振る舞い契約として扱う。
- 実装は、stories / tests のレビュー完了後に開始する。
- Storybook は最終的なUI確認およびレビュー環境として扱う。

stories は UI仕様、
tests は振る舞い契約、
実装はそれらを満たすために行う。

---

## 基本原則

### コンポーネントの作成

- コンポーネントは再利用可能なUI単位として設計すること。
- コンポーネントの作成では、まず `.stories.tsx` ファイルを先に作成する。
  - APIを利用している場合は、`.stories.tsx` 内でモック化すること。
- コンポーネントが振る舞いを持つ場合のみ `.test.tsx` ファイルを追加する。
  - `.test.tsx` ファイルを追加した際は、RED（失敗状態）で作成する。
  - APIを利用している場合はモック化すること。
    - `src/lib/msw/handlers` 内にハンドラー用のファイルを作成する。
    - `src/lib/msw/setup/server.ts` でハンドラーファイルの読み込み設定を行う。
- `.stories.tsx` および `.test.tsx` のレビュー完了前に実装を開始してはいけない。
- `.stories.tsx` および `.test.tsx` 作成完了後にレビュー可能状態にすること。
- `.stories.tsx` および `.test.tsx` のレビュー完了後、それらを満たすように実装すること。
  - 実装の際は `.test.tsx` を GREEN にすること。
  - 実装完了後、Storybook で確認可能な状態にすること。
- 実装完了後、Storybook 上で確認およびレビューを行うこと。
- 各レビューで問題があった場合は、修正して再レビューを行うこと。

---

### ページコンポーネントの作成

- 必要な各種コンポーネントの作成完了後、`page.tsx` に配置するページコンポーネントを作成すること。
- ページコンポーネントの作成では、`.stories.tsx` および `.test.tsx` を作成する。
  - APIを利用している場合はモック化すること。
  - `.test.tsx` はインテグレーションテストを含めること。
  - `.test.tsx` は RED（失敗状態）で作成すること。
- `.stories.tsx` および `.test.tsx` のレビュー完了前に実装を開始してはいけない。
- `.stories.tsx` および `.test.tsx` 作成完了後にレビュー可能状態にすること。
- `.stories.tsx` および `.test.tsx` のレビュー完了後、それらを満たすように実装すること。
  - ページコンポーネントは原則としてサーバーコンポーネントで作成すること。
  - クライアント側で状態共有が必要な場合のみクライアントコンポーネント化を許可する。
  - 実装時は `.test.tsx` を GREEN にすること。
  - 実装完了後、Storybook で確認可能な状態にすること。
- 実装完了後、Storybook 上で確認およびレビューを行うこと。
- 各レビューで問題があった場合は、修正して再レビューを行うこと。

---

### ページファイル（page.tsx）の作成

- 必要なページコンポーネントの作成完了後、ページファイル（page.tsx）を作成すること。
- 画面表示時に必要なデータ取得は、原則として page.tsx で行うこと。
- page.tsx で取得したデータはページコンポーネントへ渡すこと。
- page.tsx に複雑なUIロジックや状態管理を書いてはいけない。
- page.tsx の責務は以下に限定すること。
  - routing
  - params handling
  - fetch
  - ページコンポーネント呼び出し
- ページファイル作成完了後、ローカルサーバーを起動して動作確認およびレビューを行うこと。

---

### e2eテストの作成

- 複数ページの作成が完了し、ユーザーのユースケースを満たせる状態になった場合、必要に応じて e2e テストを作成する。
- e2eテストは必須ではない。
- e2eテストは薄く保つこと。
- e2eテストでは実装詳細ではなく、ユーザーユースケースを検証すること。
- e2eテストファイルは `tests/e2e/` 配下に `.spec.ts` として作成すること。

---

## コンポーネントの開発フロー

1. `.stories.tsx` を作成する
  - UI状態を定義する
  - 必要に応じてモックデータを作成する
  - API利用時は Storybook 用にモック化する

2. 振る舞いが必要な場合のみ `.test.tsx` を作成する
  - RED（失敗状態）で作成する
  - interaction / state change / validation を対象とする
  - API利用時は MSW でモック化する

3. `.stories.tsx` と `.test.tsx` をレビューする
  - UI仕様
  - 状態定義
  - interaction
  - accessibility
  - responsive behavior
  - test内容
  を確認する

4. `.stories.tsx` と `.test.tsx` を満たすように実装する
  - 最小実装を優先する
  - stories / tests を書き換えて通してはいけない

5. `.test.tsx` を GREEN にする
  - 全テストを成功させる
  - flaky test を作らない

6. Storybook で確認・レビューする
  - 各状態が正しく表示されること
  - visual consistency
  - accessibility
  - responsive behavior
  を確認する

7. 必要に応じてリファクタリングする
  - stories
  - tests
  を壊さないこと

---

## ページコンポーネントの開発フロー

1. 必要なコンポーネントを組み合わせて設計する

2. `.stories.tsx` を作成する
  - ページ全体のUI状態を定義する
  - Loading / Empty / Error を含める
  - API利用時はモック化する

3. `.test.tsx` を作成する
  - インテグレーションテストを含める
  - RED（失敗状態）で作成する

4. `.stories.tsx` と `.test.tsx` をレビューする
  - ユースケース
  - 状態遷移
  - interaction
  - accessibility
  - responsive behavior
  を確認する

5. `.stories.tsx` と `.test.tsx` を満たすように実装する
  - 原則としてサーバーコンポーネントで作成する
  - 必要な場合のみクライアントコンポーネント化する

6. `.test.tsx` を GREEN にする
  - インテグレーションテストを通す
  - 実装依存テストを避ける

7. Storybook で確認・レビューする
  - ページ全体の表示
  - 各状態
  - responsive behavior
  を確認する

8. 必要に応じてリファクタリングする

---

## ページファイル（page.tsx）の開発フロー

1. 必要なページコンポーネントを完成させる

2. `page.tsx` を作成する
  - routing
  - params handling
  - fetch
  - ページコンポーネント呼び出し
  のみを担当する

3. 画面表示時に必要なデータをフェッチする
  - 原則として page.tsx でフェッチする
  - フェッチしたデータをページコンポーネントへ渡す

4. loading / error handling を実装する

5. ローカルサーバーで確認・レビューする
  - routing
  - navigation
  - fetch
  - rendering
  を確認する

6. 必要に応じてリファクタリングする

---

## e2eテストの開発フロー

1. 対象ユースケースを整理する
  - ユーザー操作
  - 主要画面遷移
  - 主要機能
  を対象とする

2. `tests/e2e/` 配下に `.spec.ts` を作成する

3. ユースケースベースでテストを書く
  - 実装詳細に依存しない
  - UI内部構造に依存しない

4. 必要最小限のテストにする
  - e2eテストは薄く保つ
  - 重複テストを避ける

5. ローカル実行で確認する
  - 画面遷移
  - 入力
  - API連携
  - エラーハンドリング
  を確認する

6. flaky test を作らない
  - wait乱用禁止
  - timeout依存禁止
  - 安定した selector を使用する

---

## 完了条件

### コンポーネント

- `.stories.tsx` 作成済み
- 必要なUI状態が定義済み
- review 完了
- 実装完了
- Storybook で確認済み
- `.test.tsx` がある場合は GREEN

---

### ページコンポーネント

- `.stories.tsx` 作成済み
- Loading / Empty / Error 状態を確認済み
- review 完了
- 実装完了
- integration test がある場合は GREEN
- responsive behavior 確認済み
- Storybook で確認済み

---

### ページファイル（page.tsx）

- App Router に統合済み
- fetch 処理確認済み
- loading / error handling 実装済み
- ローカルサーバーで動作確認済み

---

### e2eテスト

- 必要なユースケースをカバー
- flaky test が存在しない
- ローカル実行で成功</code></pre>
</div>
<p>&nbsp;</p>
<h3>Agent Skillsを追加（任意）</h3>
<p>必要に応じてAgent Skillsを使うことで効率化が図れますが、例えば以下のようなスキルを利用したりします。</p>
<p>&nbsp;</p>
<h4>・スキル「plan-to-issue」：プランモードで作成した開発計画をGitHubのIssue用のフォーマットへ変換して自動登録する</h4>
<p>まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/skills/plan-to-issue &amp;&amp; touch .codex/skills/plan-to-issue/SKILL.md
$ mkdir -p .codex/skills/plan-to-issue/references &amp;&amp; touch .codex/skills/plan-to-issue/references/rules.md
$ mkdir -p .codex/skills/plan-to-issue/assets &amp;&amp; touch .codex/skills/plan-to-issue/assets/template.md
$ mkdir -p .codex/skills/plan-to-issue/scripts &amp;&amp; touch .codex/skills/plan-to-issue/scripts/create_issue.sh
$ chmod +x .codex/skills/plan-to-issue/scripts/create_issue.sh</code></pre>
</div>
<p><span style="color: #ff0000;">※スクリプトは「chmod +x」で実行権限を付与します。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「.codex/skills/plan-to-issue/SKILL.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/plan-to-issue/SKILL.md"><code>---
name: plan-to-issue
description: プランモードで作成した開発計画をGitHub Issue用フォーマットへ変換して自動登録する
---

# plan-to-issue

## 概要

このスキルは、プランモードで作成された開発計画を読み込み、定義された変換ルールに従ってGitHub Issue形式へ構造化し、自動的にGitHubへ登録する。

---

## 重要原則（最重要）

- summary（概要）は要約してよい
- tasks（タスク）は**絶対に要約してはならない（展開必須）**
- tasksは「実装可能な手順レベル」にまで分解する
- 不明点は補完しつつも、実行手順を失わないことを優先する

---

## 参照ファイル

必ず以下を読み込むこと：

- **変換ルール定義**: `references/rules.md`
- **出力テンプレート**: `assets/template.md`
- **登録スクリプト**: `scripts/create_issue.sh`

---

## 入力

プランモードで作成された開発計画（仕様・タスク・背景など）

---

## 出力

GitHub Issue（gh issue create により自動登録）

---

## 実行手順

### 1. 開発計画の解析

- 目的・背景・機能・制約を分解して理解する
- 暗黙的なタスクも抽出する

---

### 2. ルール読み込み

- `references/rules.md` を厳密に適用する

---

### 3. テンプレート適用

- `assets/template.md` に従いIssue構造を生成

---

### 4. タイトル生成

- rules.mdの命名規則に従う

---

### 5. GitHub登録

- `scripts/create_issue.sh` を使用してIssue作成

---

## 品質保証（必須チェック）

- tasksは抽象化されていないか
- 実装手順が復元可能か
- 設計・実装・テストが含まれているか</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/plan-to-issue/references/rules.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/plan-to-issue/references/rules.md"><code># 変換ルール定義（plan-to-issue）

## 1. 基本方針

- 1 Issue = 1目的（または1成果物）
- 情報は構造化するが、**実行可能性を失わないことを最優先**
- summaryとtasksは役割を完全に分離する

---

### 最重要ルール

#### summaryとtasksの役割分離

| 項目 | 性質 | ルール |
|------|------|--------|
| summary | 要約 | 圧縮OK |
| tasks | 実行手順 | 圧縮禁止・完全展開必須 |

---

## 2. フィールド対応ルール

### 2-1 summary（概要）

- 目的ベースで1〜3文
- 実装詳細は書かない
- 要約OK

---

### 2-2 background（背景）

- 課題・理由・現状を記述
- 不足は文脈補完可

---

### 2-3 scope（スコープ）

- In Scope / Out of Scope を明示
- 暗黙スコープは必ず明文化

---

## 2-4 tasks

### 絶対ルール

#### 禁止

- 「実装する」
- 「対応する」
- 「作る」
- 「改善する」

→ これらはすべて禁止（抽象語）

---

### 必須ルール

tasksは必ず以下を満たす：

#### 1. 手順分解必須
- 設計 → 実装 → テスト → 検証の順で展開

#### 2. 最小粒度
- 1タスク = 30〜90分単位
- 1タスク = 単一アクション

#### 3. 具体動詞必須
- 「作成する」「定義する」「追加する」「検証する」

---

#### 4. 必須分解カテゴリ

必ず以下を含める：

- 仕様確認
- 設計
- データ構造定義
- 実装
- エラーハンドリング
- テスト
- 動作確認

---

#### 5. タスク展開ルール（強化）

開発計画からタスク化する際は：

- 機能を「手順」に分解
- 暗黙工程も明示化
- 一つの機能を1タスクにしない

---

#### 6. 良い例

##### 悪い
- APIを実装する

##### 良い
- API仕様（リクエスト/レスポンス）を定義する
- エンドポイントルーティングを作成する
- バリデーション処理を実装する
- DBアクセス層を実装する
- エラーハンドリングを追加する
- 単体テストを作成する
- 結合テストを実行する

---

## 2-5 acceptance_criteria

- 「できること」で記述
- 必ずテスト可能であること

例：
- 正常リクエストで200が返ること
- 不正入力でエラーが返ること

---

## 2-6 notes

- 技術制約
- 依存関係
- 未確定事項（要確認）
- 将来対応

---

## 3. タイトル生成ルール

Issueタイトルは以下のルールで生成する

### フォーマット

[種別] 内容の要約

### 種別の分類

- feat: ユーザーに価値を提供する新機能
- fix: 不具合の修正
- refactor: 挙動を変えない内部改善
- docs: ドキュメントの追加・更新
- test: テストの追加・修正
- infra: インフラ・CI/CD・環境構築
- chore: 上記に当てはまらない雑務（極力使わない）

### ルール

- 30〜60文字程度に収める
- 内容が一目で分かるようにする
- summaryの内容をベースに生成する

例:

- feat: ユーザー登録APIを実装する
- fix: ログイン時の認証エラーを修正

---

## 4. Issue分割ルール

- 10タスク以上 → 分割必須
- 独立機能は別Issue
- 依存関係はnotesへ

---

## 5. 不足情報補完

- 合理的推測OK
- 推測はnotesに明記
- 不確定は「要確認」

---

## 6. 禁止事項

- 抽象タスク
- 実行不能なタスク
- tasksの要約
- 背景なしIssue

---

## 7. 出力品質チェック

- tasksが完全手順化されている
- 抽象名詞が存在しない
- 設計〜テストが揃っている
- 実装順が復元可能</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/plan-to-issue/assets/template.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/plan-to-issue/assets/template.md"><code>## 概要
{{summary}}

## 背景
{{background}}

## スコープ
{{scope}}

## タスク
- [ ] {{tasks}}

## 受け入れ条件
- {{acceptance_criteria}}

## 補足
{{notes}}</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/plan-to-issue/scripts/create_issue.sh」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash" data-file=".codex/skills/plan-to-issue/scripts/create_issue.sh"><code>#!/usr/bin/env bash

set -euo pipefail

# ===============================
# Usage:
# ./create_issue.sh "タイトル" "本文"
# ===============================

TITLE="${1:-}"
BODY="${2:-}"

# タイトルと本文の入力チェック
if [[ -z "$TITLE" || -z "$BODY" ]]; then
  echo "Usage: $0 \"タイトル\" \"本文\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &amp;&gt; /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &amp;&gt; /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "Issueを作成中..."

# 一時ファイル作成
TMP_FILE=$(mktemp)

# 一時ファイルをスクリプト終了時に削除
trap 'rm -f "$TMP_FILE"' EXIT

# 本文を一時ファイルに書き込み
printf "%s" "$BODY" &gt; "$TMP_FILE"

# Issue作成
gh issue create \
  --title "$TITLE" \
  --body-file "$TMP_FILE"

echo "Issue作成完了"</code></pre>
</div>
<p>&nbsp;</p>
<h4>・スキル「auto-commit」：修正したコードをステージングしたうえで差分を解析し、適切なコミットメッセージを生成してgit commitまでを自動で実行する</h4>
<p>次に以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/skills/auto-commit &amp;&amp; touch .codex/skills/auto-commit/SKILL.md</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「.codex/skills/auto-commit/SKILL.md」</p>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/auto-commit/SKILL.md"><code>---
name: auto-commit
description: 修正したコードをステージングしたうえで差分を解析し、適切なコミットメッセージを生成してgit commitまでを自動で実行する
---

# auto-commit

## 概要
このスキルは、コード修正後の未ステージ状態から変更を検出し、自動でステージング、差分解析、コミットメッセージ生成、git commit実行までを一貫して行う。

## 処理フロー

### 1. 未ステージの変更をステージ

以下のコマンドを実行し、未ステージの変更をすべてステージする。

```bash
git add -A
```

---

### 2. ステージ済み差分の取得

以下のコマンドを実行し、ステージ済みの変更内容の差分情報を取得する。

```bash
git diff --cached
```

---

### 3. 差分解析

取得したコードの差分情報から以下を解析する：

- 変更の目的（機能追加 / バグ修正 / リファクタリング / 雑務）
- 影響範囲（ファイル・モジュール）
- ユーザー視点での変化
- 変更が単一責務かどうか

---

### 4. コミットメッセージ生成

コードの差分情報の解析結果をもとに、
Conventional Commits形式でコミットメッセージを生成する。

#### フォーマット

`&lt;type&gt;: &lt;summary&gt;`

#### type一覧

- feat: ユーザーに価値を提供する新機能
- fix: 不具合の修正
- refactor: 挙動を変えない内部改善
- docs: ドキュメントの追加・更新
- test: テストの追加・修正
- infra: インフラ・CI/CD・環境構築
- chore: 上記に当てはまらない雑務（極力使わない）

#### コミットメッセージの例

fix: ログイン時のトークン更新処理の不具合を修正

---

### 5. コミット実行

生成したコミットメッセージを用いて以下のコマンドを実行し、コミットを実行する。

```bash
git commit -m "&lt;generated commit message&gt;"
```

---

## 動作ルール

### 0. 実行順序

- 処理は必ず「git add → diff確認 → commit可否判定」の順で実行する
- git add は処理の最初に一度だけ実行する（追加実行は禁止）

---

### 1. 変更検知・終了条件

- 変更が存在しない場合は、git add / commit は実行せず処理を終了する
- git diff --cached の結果が空の場合も同様にコミットを行わない
- git add 後に再度差分を確認し、変更がない場合は即終了する
- 空コミットは絶対に作成しない

---

### 2. 安全性チェック（危険変更の制御）

- .env, secrets, credential系ファイルの変更が含まれる場合は必ず停止する
- 破壊的変更（大量削除・ファイル削除が多数）の場合は警告を出し、停止する

- 破壊的変更の定義：
  - 削除ファイルが5件以上
  - または差分行数の削除が追加の2倍以上

- 上記条件を満たす場合は自動実行せず、必ず停止してユーザー確認を求める

---

### 3. コミット構造ルール

- 1つのコミットは必ず単一の目的（単一責務）になるようにする
  - 複数の変更目的が混在している場合は論理的にまとめるか警告扱いとする

- 大規模変更（複数モジュールにまたがる変更）の場合は、可能であれば分割コミットを優先する

---

### 4. コミットメッセージ生成ルール

- コミットメッセージは必ず変更内容に基づいて生成し、推測や一般論で補完しない
  - 差分から読み取れない情報は含めない

- メッセージは簡潔にしつつ、変更の「意図」が分かる表現にする
  - 実装内容の羅列ではなく、何が改善されたかを優先する

- コミットメッセージは50〜72文字程度を目安にする

- 英語・日本語のどちらを使うかはプロジェクトの既存コミットに合わせて統一する

---

### 5. 変更分類ルール

- 変更内容が以下に該当する場合は chore に分類する
  - フォーマット修正のみ
  - コメント修正のみ
  - 空白・改行整理のみ
  - 自動生成ファイルの更新

---

### 6. コミット実行ルール

- commit 実行前に最終的なメッセージを内部で確定させること
- git commit は確定したメッセージのみで実行する

---

## まとめ

このスキルは以下を保証する：

- 安全な自動コミット（事故防止）
- 意味ベースのコミット生成
- 単一責務の維持
- 変更意図の明確化
- チーム規約との整合性</code></pre>
</div>
</div>
<p>&nbsp;</p>
<h4>・スキル「auto-pr」：直前のコミット内容をもとにPRタイトルと本文を生成し、GitHubのPR用フォーマットへ変換して自動登録する</h4>
<p>次に以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/skills/auto-pr &amp;&amp; touch .codex/skills/auto-pr/SKILL.md
$ mkdir -p .codex/skills/auto-pr/assets &amp;&amp; touch .codex/skills/auto-pr/assets/template.md
$ mkdir -p .codex/skills/auto-pr/scripts &amp;&amp; touch .codex/skills/auto-pr/scripts/create_pr.sh
$ chmod +x .codex/skills/auto-pr/scripts/create_pr.sh</code></pre>
</div>
<p><span style="color: #ff0000;">※スクリプトは「chmod +x」で実行権限を付与します。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「.codex/skills/auto-pr/SKILL.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/auto-pr/SKILL.md"><code>---
name: auto-pr
description: 直前のコミット内容をもとにPRタイトルと本文を生成し、GitHubのPR用フォーマットへ変換して自動登録する
---

# auto-pr

## 概要

このスキルは、直前のコミット内容をもとにPRタイトルと本文を生成し、GitHubのPR用フォーマットへ変換して自動登録します。

## 参照ファイル

このスキルを実行する際は、以下のファイルを必ず読み込んで使用してください。

- PRテンプレート: `assets/template.md`
- PR作成スクリプト: `scripts/create_pr.sh`

## 入力

直前のコミット情報

## 出力

GitHubのPR（gh pr create により自動登録）

---

## 実行手順

### 1. 直前のコミット情報と変更内容を取得する

```bash
git log -1 --pretty=format:"%h%n%s%n%b"
git show --no-color
```

- git log: 意図（タイトル・背景）
- git show: 変更内容（diff）

---

### 2. 解析して以下を生成する

- summary（概要）
- background（背景・目的）
- implementation（実装内容：箇条書き）
- test_items（テスト確認項目：チェックリスト形式）
- impact（影響範囲：箇条書き）
- future_work（未対応・今後の課題：箇条書き or なし）

---

### 3. PR本文を生成する

`assets/template.md` を読み込み、以下ルールで置換する：

- {{summary}} → summary
- {{background}} → background
- {{implementation}} → implementation
- {{test_items}} → test_items
- {{impact}} → impact
- {{future_work}} → future_work

---

### 4. PRタイトルを生成する

フォーマット：

```text
auto: &lt;summaryの短縮版&gt;
```

---

### 5. PRを作成する

現在のブランチをGitHubへ反映し、そのブランチを元にPRを作成する。

#### 5-1. 現在のブランチをGitHubへプッシュ

```bash
git push -u origin HEAD
```

※ pushに失敗した場合は処理を中断する

#### 5-2. PR作成

`scripts/create_pr.sh` を利用し、生成したPRタイトルと本文をGitHubに登録する

---

## 補足ルール（重要）

### ■ implementation

- diff（git show）に**実際に現れた変更のみ**を記述する
- 推測は禁止

---

### ■ test_items（重要改善）

テスト観点は以下を厳守：

- コマンド実行（例: pnpm test, npm run test など）を書かない
- 「実行手段」ではなく「確認すべき振る舞い」を書く
- diffに基づいたユーザー視点の確認項目にする

#### 良い例

- [ ] ユーザー一覧が正しく取得できる
- [ ] エラー時に適切なメッセージが表示される
- [ ] 新規作成後に一覧へ反映される

#### 悪い例（禁止）

- [ ] pnpm testを実行する
- [ ] テストコマンドを通す

---

### ■ impact

- 影響範囲を事実ベースで記載する（推測禁止）

---

### ■ future_work（重要改善）

以下ルールを厳守：

- diffやコミット内容から**明確に未対応と分かる場合のみ記載**
- 推測や一般論は禁止
- 該当がない場合は必ず以下のいずれかにする：
  - `なし`
  - または空欄（推奨は「なし」）

#### 記載してよい例

- エラーハンドリングの追加が未実装
- 一部APIのリトライ処理が未対応

#### 記載NG例

- パフォーマンス改善が必要（根拠なし）
- 今後リファクタリング予定（根拠なし）

---

## 全体ルール

- implementation / impact / future_work は必ず箇条書き
- test_items は必ずチェックリスト形式（- [ ]）
- diff（git show）を必ず根拠とし、推測のみで生成しない
</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/auto-pr/assets/template.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/auto-pr/assets/template.md"><code>## 概要
{{summary}}

## 背景・目的
{{background}}

## 実装内容
- {{implementation}}

## テスト確認項目
- [ ] {{test_items}}

## 影響範囲
- {{impact}}

## 未対応・今後の課題
- {{future_work}}</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/auto-pr/scripts/create_pr.sh」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash" data-file=".codex/skills/auto-pr/scripts/create_pr.sh"><code>#!/usr/bin/env bash

set -euo pipefail

# ===============================
# Usage:
# ./create_pr.sh "タイトル" "本文"
# ===============================

TITLE="${1:-}"
BODY="${2:-}"

# タイトルと本文の入力チェック
if [[ -z "$TITLE" || -z "$BODY" ]]; then
  echo "Usage: $0 \"タイトル\" \"本文\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &amp;&gt; /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &amp;&gt; /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "PRを作成中..."

# 一時ファイル作成
TMP_FILE=$(mktemp)

# 一時ファイルをスクリプト終了時に削除
trap 'rm -f "$TMP_FILE"' EXIT

# 本文を一時ファイルに書き込み
printf "%s" "$BODY" &gt; "$TMP_FILE"

# PR作成
gh pr create \
  --title "$TITLE" \
  --body-file "$TMP_FILE"

echo "PR作成完了"</code></pre>
</div>
<p>&nbsp;</p>
<h3>今回のプロジェクト用のハーネス設定</h3>
<p>次に今回のプロジェクト用のハーネス設定を行いますが、<strong></strong><span style="background-color: #ffc6d1;"><strong>今回はNext.jsかつSDD（Storybook Driven Development）およびTDD（Test Driven Development）を採用</strong></span>したやり方で開発をすることを想定しています。</p>
<p>今回はそれらを踏まえて各種定義ファイルを作成していく必要があるため、まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p docs/rules
$ touch docs/rules/ui.md docs/rules/testing.md docs/rules/frontend.md
$ touch src/AGENTS.md AGENTS.md</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はできるだけ最小構成になるようにしています。ファイル数を増やしてしっかり定義した方が、より想定外の動作やリスクを防ぐことができるようになります。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「docs/rules/ui.md」（UI実装ルール）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="docs/rules/ui.md"><code># UI実装ルール（Storybook）

Storybookを利用したUI実装ルールを定義する。

Storybookは単なる動作確認環境ではなく、UI仕様書およびレビュー環境として扱う。

---

## 基本原則

- stories を先に作成する
- stories はUI仕様書として扱う
- Storybook 単体でレビュー可能な状態にする
- 実装前に stories をレビュー可能状態にする
- stories を満たすように実装する
- UI状態を省略しない
- mock を利用して再現可能な状態を作る
- UI状態は useEffect で作らない

---

## stories の責務

stories は以下を表現すること：

- UI状態
- interaction
- Loading状態
- Empty状態
- Error状態
- Disabled状態
- responsive behavior
- accessibility

---

## stories 作成ルール

### 1 story = 1 state

1つの story では1つの状態のみを表現すること。

悪い例：

```tsx
export const AllStates = () =&gt; (
  &lt;&gt;
    &lt;Button /&gt;
    &lt;Button disabled /&gt;
    &lt;Button loading /&gt;
  &lt;/&gt;
);
```

良い例：

```tsx
export const Default = {};
export const Loading = {};
export const Error = {};
export const Empty = {};
export const Disabled = {};
```

---

## 必須状態

必要に応じて以下を作成すること：

- Default
- Loading
- Empty
- Error
- Disabled
- Hover
- Active
- Selected
- Mobile
- Tablet
- Desktop
- Dark mode

---

## args を利用する

props 切り替え可能なものは args を利用すること。

固定値を大量に埋め込まないこと。

---

## controls を利用する

レビュー時に変更可能な props は controls を利用すること。

---

## mock データルール

- Storybook 上で完結するモックを作成する
- API通信を実際に行ってはいけない
- API利用時は MSW を利用する
- mock は再利用可能にする

---

## MSW 利用ルール

API利用時は MSW を利用すること。

- handler は `src/lib/msw/handlers` に配置する
- Storybook 用 mock と test 用 mock を共有可能にする
- Loading / Error / Success を切り替え可能にする

---

## レイアウトルール

- Storybook 上で崩れないこと
- 極端な margin 調整に依存しないこと
- 固定高さレイアウトを乱用しないこと

---

## responsive ルール

responsive behavior を確認可能にすること。

最低限確認する：

- Mobile
- Desktop

必要に応じて Tablet も確認すること

---

## accessibility ルール

以下を確認すること：

- keyboard interaction
- focus visibility
- aria-label
- semantic HTML
- color contrast

---

## Tailwind CSS ルール

- utility class を優先する
- className の肥大化を避ける
- 重複 class を避ける
- magic number を乱用しない
- conditional class は整理する

---

## useEffect ルール（重要）

Storybook内のUIコンポーネントにおいて useEffect は「副作用専用」として扱う。

---

### 基本原則

- UI状態の生成に useEffect を使わない
- props → state の同期に使わない
- Storybookの状態は stories / args で表現する
- UIの表示制御目的で useEffect を使わない

---

### 使用してよいケース

- ブラウザAPI操作（window / document）
- event listener の登録・解除
- 外部購読（WebSocketなど）
- DOM操作
- アニメーション制御などの純粋な副作用

---

### 使用禁止・非推奨ケース

- propsからstateを生成する処理
- loading / error などUI状態の制御
- Storybook状態の代替としてのuseEffect
- API fetch目的（Server Component / MSW優先）

---

### Storybookとの関係

- UI状態は story で定義する
- state遷移は story 切替で表現する
- useEffectで状態遷移を作らない
- mock / args で再現する

---

### 依存配列ルール

- 使用している値は必ず依存配列に含める
- eslintルールを無視しない
- stale closure を許容しない

---

### StrictMode考慮

- useEffectは開発時に2回実行される可能性がある
- 副作用は必ず冪等にすること
- 二重実行で壊れる処理は禁止

---

### クリーンアップ必須ケース

- event listener
- interval / timeout
- subscription

必ず cleanup を実装すること

---

## Component 分割ルール

以下の場合は component 分割を検討すること：

- JSX が長すぎる
- 状態責務が増えすぎている
- 再利用可能
- story が複雑になりすぎている

---

## 禁止事項

- 実装前に stories を省略する
- Loading/Error を省略する
- Storybook 上で動作確認できない状態を作る
- API実通信に依存する
- 巨大 story を作る
- 1 story で複数状態を表現する
- UI仕様未定義のまま実装する
- useEffectでUI状態を構築する

---

## レビュー観点

レビュー時は以下を確認する：

- UI仕様が十分か
- 状態が不足していないか
- responsive behavior
- accessibility
- visual consistency
- interaction
- Loading/Error handling
- useEffectの用途が適切か</code></pre>
</div>
<p>&nbsp;</p>
<p>・「docs/rules/testing.md」（テスト実装ルール）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="docs/rules/testing.md"><code># テスト実装ルール

フロントエンド開発におけるテスト実装ルールを定義する。

- tests は振る舞い契約として扱う
- 実装詳細ではなく、ユーザーから見た振る舞いを保証することを目的とする

---

## 基本原則

- 必要な場合のみ tests を作成する
- tests は RED（失敗状態）から開始する
- tests を満たすように実装する
- 実装詳細ではなく振る舞いをテストする
- flaky test を作らない
- mock を利用して安定したテストを作る

---

## test が必要な条件

以下を含む場合は `.test.tsx` を作成すること：

- ユーザー操作
- state変更
- form validation
- conditional rendering
- async処理
- accessibility interaction
- integration behavior

---

## test が不要なケース

以下のみの場合は test を省略可能：

- 単純な Presentational Component
- Typography wrapper
- Layout wrapper
- 静的表示のみ

---

## Unit Test ルール

Unit Test では以下を確認すること：

- interaction
- state change
- rendering
- callback
- validation

---

## Integration Test ルール

Integration Test では以下を確認すること：

- Component連携
- 状態遷移
- API integration
- form flow
- user flow

---

## E2E Test ルール

E2E Test は以下を目的とする：

- 主要ユースケース確認
- 回帰防止
- 画面統合確認

E2E Test は薄く保つこと

---

## selector ルール

selector の優先順位は以下とする：

1. role
2. label
3. text
4. placeholder
5. data-testid

可能な限り `data-testid` を避けること

---

## Mocking ルール

API利用時は MSW を利用すること。

- mock を再利用可能にする
- Storybook と test で共有可能にする
- Success / Error / Loading を切り替え可能にする

---

## MSW 利用ルール

- handler は `src/lib/msw/handlers` に配置する
- `src/lib/msw/setup/server.ts` に登録する
- 実API通信を行ってはいけない

---

## useEffect テスト上のルール（重要）

テストにおいて useEffect を含むコンポーネントは「副作用の発火タイミング」に依存しない設計にすること。

---

### 基本原則

- useEffectの実行タイミングをテストで制御しようとしない
- useEffectの内部状態を直接検証しない
- 副作用の結果のみを検証する
- render後の不安定な状態変化を前提にしない

---

### テストしてよい対象

- 副作用の「結果」（UI変化・DOM変化）
- event handler による状態変化
- ユーザー操作後の挙動

---

### テストしてはいけない対象

- useEffect の実行回数
- useEffect 内部のローカル変数
- useEffect のタイミング依存ロジック
- 非同期タイマーの内部状態

---

### 非同期 useEffect の扱い

- waitFor は最小限にする
- 安定したUI結果を待つ
- setTimeout 依存のテストは禁止
- fetch結果は MSW で制御する

---

## 非同期テストルール

- async/await を利用する
- waitFor を最小限にする
- 安定した完了条件を待つ
- timing依存を避ける

---

## accessibility テストルール

必要に応じて以下を確認すること：

- keyboard navigation
- focus movement
- aria attributes
- accessible name

---

## flaky test 防止ルール

- ランダム値に依存しない
- 時刻依存を避ける
- timeout依存を避ける
- 実ネットワーク通信を行わない
- test 間で状態共有しない
- useEffectの非同期タイミングに依存しない

---

## RED / GREEN ルール

### RED

- 失敗する test を先に作成する
- 未実装状態であることを確認する

### GREEN

- 最小実装で test を通す
- test を書き換えて通してはいけない

---

## 禁止事項

- className selector
- implementation detail のテスト
- state 内部値の直接確認
- private function のテスト
- snapshot test の乱用
- waitFor の乱用
- timeout 依存
- sleep/wait の固定時間依存
- useEffectの内部実行制御を前提にしたテスト

---

## レビュー観点

レビュー時は以下を確認すること：

- 振る舞いを保証できているか
- implementation detail に依存していないか
- flaky test の可能性がないか
- accessibility を考慮しているか
- 過剰テストになっていないか
- useEffect依存のテスト設計になっていないか</code></pre>
</div>
<p>&nbsp;</p>
<p>・「docs/rules/frontend.md」（フロントエンド開発の実装ルール）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="docs/rules/frontend.md"><code># フロントエンド開発の実装ルール（Next.js / App Router）

Next.js の App Router を利用したフロントエンド開発の実装ルールを定義する。

各ディレクトリの責務を明確に分離し、UI・状態・データ取得・ルーティングの境界を整理することを目的とする。

---

## 基本原則

- App Router を利用する
- page.tsx は薄く保つ
- UI責務とデータ責務を分離する
- 再利用可能なUIは `src/components` に配置する
- API通信は直接 component に埋め込まない
- Server Component を優先する
- 必要な場合のみ Client Component を利用する
- Storybook でレビュー可能な構成にする

## ディレクトリ責務

### `src/app`

Next.js App Router 用ディレクトリ。

- routing
- page.tsx
- layout.tsx

---

### `src/components`

コンポーネントを管理する。

- UI Components
- Page Components
- stories
- tests

---

### `src/components/common`

共通利用するコンポーネントパーツを管理する。

- Button
- Input
- Dialog
- Card
- Table
- Form Parts

再利用可能なUIはここに配置すること。

---

### `src/contexts`

アプリ全体で共有する状態を管理する。

#### 役割

- グローバル状態管理
- アプリ横断の状態共有

#### 例

- Auth
- Theme
- Locale
- UI設定

#### 禁止事項

- フォーム状態の管理
- 一時的UI状態の管理
- コンポーネント内部状態の代替

---

### `src/hooks`

Reactロジックの再利用を管理する。

#### 役割

- UIロジックの再利用
- interactionロジックの共通化

#### 例

- useModal
- useDebounce
- useMediaQuery
- usePagination

#### 禁止事項

- UIコンポーネントの定義
- pure function（→ libへ）
- APIクライアント（→ libへ）

---

### `src/types`

アプリ全体の型定義を管理する。

#### 役割

- APIレスポンス型
- ドメイン型
- 共通UI型
- フォーム型

---

### `src/lib`

アプリに依存しない純粋な基盤コードを管理する。

#### 役割

- APIクライアント
- APIエンドポイント定義
- utils
- constants
- validators
- MSW

---

### `src/lib/api`

API関連処理を管理する。

#### 構成例

- client.ts（fetchラッパー）
- endpoints.ts（エンドポイント定義）
- users.ts（ドメイン別API）
- auth.ts

---

### `src/lib/api/endpoints.ts`

APIエンドポイントを一元管理する。

#### 目的

- URLの散在防止
- 変更の局所化
- API呼び出しの統一

#### 例

```tsx
export const endpoints = {
  users: "/api/users",
  userDetail: (id: string) =&gt; `/api/users/${id}`,
  login: "/api/auth/login",
};
```

#### 禁止事項

- コンポーネント内でURLを直接記述すること
- feature/component側でのエンドポイント定義

---

### `src/lib/msw`

MSW の設定および共通基盤。

---

### `src/lib/msw/handlers`

APIごとの mock handler を管理する。

- users.ts
- auth.ts
- posts.ts

---

### `src/lib/msw/setup/server.ts`

MSW server 設定ファイル。

handler をまとめて読み込む。

---

## App Router ルール

### page.tsx の責務

- routing
- params handling
- fetch
- Page Component 呼び出し

---

## page.tsx の禁止事項

- UIロジックの実装
- 大量の JSX
- state管理
- business logic

---

## Server Component ルール

優先して利用する。

- page.tsx
- Page Components
- fetch専用コンポーネント

---

## Client Component ルール

以下が必要な場合のみ `"use client"` を使用する。

- browser API
- event handler
- useState / useEffect
- client interaction

---

## useEffect ルール（重要）

useEffectは「副作用専用の最終手段」として扱う。

### 基本原則

- UI状態の同期目的では使用しない
- props → state のコピーに使用しない
- 可能な限り Server Component または render計算で代替する
- Reactのレンダリング結果で表現できるものには使わない

---

### 使用してよいケース

- ブラウザAPI操作（window / document）
- event listener の登録・解除
- 外部サービス購読（WebSocketなど）
- DOM依存処理
- クライアント限定の副作用

---

### 使用禁止・非推奨ケース

- propsからstateを生成する処理
- 表示状態の同期（derived state）
- 初期化ロジックの代替
- fetch目的での乱用（Server Component優先）

---

### fetchとの関係

- データ取得は原則 Server Component で行う
- useEffectでのfetchは例外的ケースのみ
- useEffect fetchはUI依存・遅延処理のみ許可

---

### 依存配列ルール

- 使用している値は必ず依存配列に含める
- eslint警告を無視しない
- stale closureを許容しない設計にする

---

### StrictMode考慮

- useEffectは開発時に2回実行される可能性がある
- 副作用は冪等性を持たせること
- 二重実行で破綻する処理は禁止

---

### クリーンアップ必須ケース

- event listener
- interval / timeout
- subscription

必ず cleanup を実装すること

---

## 状態管理ルール

状態はスコープを最小にする。

優先順位：

1. local state
2. component state
3. page state
4. contexts（必要時のみ）

---

## fetch ルール

原則として page.tsx で fetch を行う。

---

## 禁止事項

- page.tsx の肥大化
- 不要な `"use client"`
- component 内での直接API実通信
- UIとfetch責務の混在
- 巨大component
- useEffectによる過剰な状態管理

---

## レビュー観点

- 責務分離できているか
- page.tsx が薄いか
- Server/Client境界が適切か
- Storybookでレビュー可能か
- test可能な構造か
- 再利用可能か
- useEffectが適切な用途か</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/AGENTS.md」（フロントエンド開発の全体ルール）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="src/AGENTS.md"><code># フロントエンド開発の全体ルール

Next.js（App Router）におけるフロントエンド開発の全体ルールを定義する。

Codex / AIエージェントを含むすべての実装は、必ずこのルールを前提として動作すること。

## 基本思想

本プロジェクトは以下を前提とする：

- Storybook Driven Development（SDD）
- Test Driven Development（TDD）
- コンポーネント中心設計
- MSWによるAPIモック前提開発
- UIとロジックの責務分離

## 参照ドキュメント

開発時は必ず以下を参照すること：

- UI実装ルール 
  → `docs/rules/ui.md`

- テストルール 
  → `docs/rules/testing.md`

- Next.js実装境界ルール 
  → `docs/rules/frontend.md`

- 実装フロー（SDD） 
  → `.codex/workflows/sdd_flow.md`

## 開発ルールの優先順位

以下の優先順位でルールを適用する：

1. AGENTS.md（本ドキュメント）
2. docs/rules/frontend.md
3. docs/rules/ui.md
4. docs/rules/testing.md
5. .codex/workflows/sdd_flow.md

## 実装フロー

実装手順の詳細は以下を参照すること：

→ `.codex/workflows/sdd_flow.md`

すべての実装は必ずこのフローに従うこと。

## ディレクトリルール

### UI

- `src/components`：UI全般
- `src/components/common`：共通UI

---

### 状態管理

- `src/contexts`：グローバル状態
- `src/hooks`：Reactロジック

---

### 型

- `src/types`：全体型定義

---

### 基盤

- `src/lib`：純粋関数・基盤処理
- `src/lib/api`：API関連
- `src/lib/msw`：モックサーバー

---

## 実装ルール

### UI実装

- 必ず Storybook を先に作成する
- stories はUI仕様書として扱う
- UI状態を省略しない
- MSWでAPIをモック化する

---

### テスト

- 振る舞いがある場合のみ test を作成する
- RED状態から開始する
- MSWを利用する
- 実装依存テストを避ける

---

### page.tsx ルール

- routingのみ担当
- fetchのみ担当
- UIロジックは禁止
- state禁止
- Page Component呼び出しのみ

---

### Server / Client ルール

#### 1. Server Component 優先

- page.tsx
- Page Components
- fetch処理

#### 2. Client Component 条件

以下の場合のみ `"use client"` を許可：

- useState / useEffect
- browser API
- event handler
- client interaction

---

## 禁止事項

以下は禁止：

- storiesなしでの実装開始
- testなしでの振る舞い実装
- APIの直接呼び出し（components内）
- UIとfetchの混在
- page.tsxの肥大化
- 不要な `"use client"`
- URLの直接記述（lib/api/endpoints以外）

## APIルール

### エンドポイント管理

APIのエンドポイントは必ず以下で管理する：

→ `src/lib/api/endpoints.ts`

#### 例

```tsx
export const endpoints = {
  users: "/api/users",
  userDetail: (id: string) =&gt; `/api/users/${id}`,
  login: "/api/auth/login",
};
```

---

### APIクライアントルール（重要）

Next.js の標準 `fetch` を優先するため、APIクライアントは必須ではない。

共通処理が必要な場合のみ、`src/lib/api/client.ts` に薄く実装する。

---

#### 役割

- エラーハンドリング統一
- JSON変換補助
- 認証ヘッダー付与
- ログ処理（必要時）

---

#### 原則

- Server Componentでは直接 fetch を使用する
- 無理にラップしない
- Next.js の fetch のキャッシュ機能を損なわない

---

### API呼び出しルール

- UI側から直接fetch禁止
- ドメインごとにAPIファイルを分割する
- endpoint は必ず `src/lib/api/endpoints.ts` を使用する

---

## MSWルール

- `src/lib/msw/handlers` にAPIモックを配置
- `src/lib/msw/setup/server.ts` で統合
- Storybook / test 両方で共有する
- 実API通信は禁止

## 品質保証ルール

コード作成・修正後は必ず以下を実行すること：

```bash
pnpm check
```

### pnpm check の目的

- フォーマット修正
- import整理
- lintチェック（静的コード解析）

## テスト実行ルール（重要）

コード修正・実装完了後は、必ず以下のテストを実行し、
すべてパスしていることを確認すること。

```bash
pnpm test
```

E2Eテストが存在する場合は、必ず以下も実行すること：

```bash
pnpm test:e2e
```

### 必須条件

- pnpm check が通ること
- pnpm test がすべてパスすること
- pnpm test:e2e がある場合はすべてパスすること
- エラー・警告が残っていないこと
- Storybookで表示崩れがないこと

## レビュー観点

AIは以下を常に確認すること：

- UI状態が不足していないか
- storiesが仕様として成立しているか
- testが振る舞いを保証しているか
- 責務分離ができているか
- fetchがUIに侵食していないか
- MSWが適切に使われているか
- page.tsxが薄いか</code></pre>
</div>
<p>&nbsp;</p>
<p>・「AGENTS.md」（全体のルール定義）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="AGENTS.md"><code># 全体のルール定義

## 概要

本プロジェクトはマルチエージェント構成（Codex Agents）を前提とする。

- 各エージェントは、`.codex/agents` に定義されており、すべての開発はこれらの役割分担に従って実行される。 
- Codex / AIエージェントは、必ずこの AGENTS.md を起点として動作する。 
- 開発の基本思想および、各エージェント、ワークフロー、開発のルール群を参照しながら開発を行うこと。

## 開発の基本思想

本プロジェクトは以下を前提とする：

- Storybook Driven Development（SDD）
- Test Driven Development（TDD）
- コンポーネント中心設計
- MSWによるAPIモック前提開発
- マルチエージェント分業開発

---

## マルチエージェント構成

以下のエージェントが役割分担して開発を行う：

- `.codex/agents/pm.toml`
- `.codex/agents/tester.toml`
- `.codex/agents/implementer.toml`
- `.codex/agents/reviewer.toml`

---

## ブランチ運用ルール（重要）

すべての実装・修正・テスト作成は、必ずブランチを切ってから開始すること。

### ブランチ命名規則

```txt
&lt;prefix&gt;/&lt;short-description&gt;
```

例

```txt
feat/user-login-ui
fix/button-disabled-state
test/login-form-validation
refactor/api-client-cleanup
docs/storybook-guideline-update
infra/msw-setup
chore/update-dependencies
```

---

### ブランチプレフィックス定義

#### 種別の分類

- feat: ユーザーに価値を提供する新機能
- fix: 不具合の修正
- refactor: 挙動を変えない内部改善
- docs: ドキュメントの追加・更新
- test: テストの追加・修正
- infra: インフラ・CI/CD・環境構築
- chore: 上記に当てはまらない雑務（極力使わない）

---

### ブランチ作成ルール（必須）

- テスト作成前に必ずブランチを作成する
- 実装開始前に必ずブランチを作成する
- 1タスク = 1ブランチを原則とする
- ブランチは短命に保つ

---

## 各エージェントの責務定義

### PM（Product Manager）

プロダクトの要件と開発方針を定義し、全体の設計意図とタスク構造を決定するエージェント。

#### 責務
- 要件整理・仕様の定義
- ユーザーストーリー作成
- タスク分解および実行順序の決定
- 全体の指揮および各エージェントへのタスク割り振り
- 必要に応じた軽量な設計判断（実装詳細には踏み込まない）

#### 禁止事項
- 実装詳細の決定
- コンポーネント構造の強制
- コードレベル設計への介入
- テストコードの作成

---

### Tester（テスター）

仕様をテストとして具体化し、期待される振る舞いを明確に定義するエージェント。

#### 参照ルール
テスト実装ルールは `docs/rules/testing.md` を参照する

#### 責務
- テスト設計（REDフェーズ）
- テストコード作成
- 期待仕様の定義（UI・挙動・状態）
- unit / integration / e2e テスト作成と実行
- APIレスポンス仕様の定義
- モックデータの設計
- MSWシナリオの定義（成功・失敗・loadingなど）

#### 禁止事項
- 実装コードの作成
- UIコンポーネント構造の決定
- 仕様変更
- テストを通すための仕様改変

---

### Implementer（実装者）

テストとStorybookで定義された仕様を満たすために実装を行うエージェント。

#### 責務
- 実装（GREENフェーズ）
- `.stories.tsx` に基づいたUI実装
- `.test.tsx` のテストを通すための最小実装
- Storybookで確認可能な状態の構築
- 既存MSWシナリオ前提での実装

#### 原則
- 最小実装を優先する
- テストを通すことが唯一の目的
- 仕様変更は禁止

#### 禁止事項
- 仕様の変更
- テストの書き換え
- MSWシナリオの変更
- 過剰設計

---

### Reviewer（レビュワー）

品質・設計・ルール遵守の最終保証を行うエージェント。

#### 責務
- テスト観点のレビュー
- StorybookでのUIレビュー
- コードレビュー
- 設計逸脱の検出
- 品質・構造の検証
- 責務分離の確認

#### 禁止事項
- 実装修正
- テスト修正
- 仕様変更

---

## 実装ワークフロー

すべての開発は以下のワークフローに従う：

```txt
.codex/workflows/sdd_flow.md
```

## フロントエンド開発の全体ルール

フロントエンド開発の実装については、以下のファイルを起点とする：

```txt
src/AGENTS.md
```

### 詳細ルール

- UI実装ルール：`docs/rules/ui.md`
- テストルール：`docs/rules/testing.md`
- フロントエンド開発の実装ルール：`docs/rules/frontend.md`

---

## 開発フロー

1. PM：要件整理・タスク分解
2. PM：ユーザーストーリー作成
3. Tester：ブランチを切る
4. Tester：テスト設計・テストコード作成（RED）
5. Reviewer：テストコードレビュー
6. Implementer：実装（GREEN）
7. Tester：テスト実行による検証およびレビュー
8. Reviewer：最終レビュー
  - 要件を満たしているか確認
  - Storybookでコンポーネント確認（UIチェック）
  - ローカルサーバーを起動してブラウザで確認（UI・動作チェック）
    - ブラウザのデベロッパーツールのコンソールで警告やエラーを確認（必要に応じて修正する）
9. 必要に応じて修正ループ

## 禁止事項

- エージェント責務の逸脱
- implementerによる仕様変更
- tester不在の実装
- storiesなし実装
- レビューなしでの完了扱い
- UIとロジックの混在
- ブランチ未作成での作業開始

## 品質保証ルール

すべてのコード変更後は必ず以下を実行すること：

```bash
pnpm check
```

さらにテストが存在する場合：

```bash
pnpm test
```

E2Eテストが存在する場合：

```bash
pnpm test:e2e
```

---

## 必須条件

- pnpm check が通ること
- pnpm test がすべてパスすること
- pnpm test:e2e がある場合はすべてパスすること
- Storybookで破綻がないこと
- レビューで承認されていること</code></pre>
</div>
<p>&nbsp;</p>
<h3>ハーネス設計の注意点</h3>
<p>上記では<strong><span style="color: #ff0000;">様々なドキュメントを定義してハーネス設計</span></strong>を行なっていますが、<strong><span style="color: #ff0000;">それぞれのドキュメントは完璧なものではありません。</span></strong></p>
<p>もし<span style="border-bottom: 2px solid #be3144;"><strong>実務でハーネス設計をする必要がある場合は、それぞれのプロジェクトに応じて最適なハーネス設計をする</strong></span>ようにして下さい。</p>
<p>そしてその際には、<span style="color: #ff0000;"><strong>システム開発におけるあらゆる知識（アーキテクチャ、設計、言語やフレームワーク、実装方法、AIツールの使い方など）がないと良し悪しが判断できない</strong></span>のと、<strong><span style="color: #ff0000;">プロジェクトに応じてどうするべきかの決断を迫られることもあると思う</span></strong>ので、その点は注意が必要です。</p>
<p>また、<strong></strong><span style="background-color: #ffc6d1;"><strong>これらのドキュメントは一回作って終わりではなく、適宜改善して育てていく必要性もある</strong></span>と思うので、その点も注意しましょう。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>GitHub ActionsによるCIの導入</h2>
<p>次にCI/CDを組み込むのも試しておきたいですが、今回は本番環境などは利用しないため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>CI（Continuous Integration：継続的インテグレーション）だけ導入</strong></span>するのを試します。</p>
<p>また、今回はコード管理にGitHubを利用しているので、相性がいい<span style="background-color: #ffc6d1;"><strong>GitHub Actionsを利用してCIを導入</strong></span>していきます。</p>
<p>まずは以下のコマンドを実行し、設定用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .github/workflows &amp;&amp; touch .github/workflows/ci.yml</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".github/workflows/ci.yml"><code>name: CI

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  changes:
    runs-on: ubuntu-latest

    outputs:
      app: ${{ steps.filter.outputs.app }}

    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v6

      - name: 差分チェック
        uses: dorny/paths-filter@v4
        id: filter
        with:
          filters: |
            app:
              - 'src/**'
              - 'tests/**'
              - 'public/**'

              # Next.js / build config
              - 'next.config.*'
              - 'tsconfig.json'
              - 'postcss.config.*'

              # package manager
              - 'package.json'
              - 'pnpm-lock.yaml'
              - 'pnpm-workspace.yaml'

              # lint / format
              - 'biome.json'

              # test tools config
              - 'vitest.config.*'
              - 'vitest.setup.*'
              - 'vitest.shims.*'
              - 'playwright.config.*'

  # biome check（フォーマット修正・import整理・lint）
  check:
    needs: changes
    if: needs.changes.outputs.app == 'true'
    runs-on: ubuntu-latest

    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v6

      - name: Node.jsのインストール
        uses: actions/setup-node@v6
        with:
          node-version: 24.15.0

      - name: Corepackの有効化
        run: corepack enable

      - name: Corepackでpnpmのインストール
        run: corepack prepare pnpm@10.33.2 --activate

      - name: pnpmの「store path」を取得
        id: pnpm-store
        run: echo "STORE_PATH=$(pnpm store path --silent)" &gt;&gt; $GITHUB_OUTPUT

      - name: pnpmのキャッシュを取得
        uses: actions/cache@v5
        with:
          path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-

      - name: .lockから依存関係をインストール
        run: pnpm install --frozen-lockfile

      - name: biome checkを実行
        run: pnpm check

  # 「unit/integration」tests 
  unit-test:
    needs: changes
    if: needs.changes.outputs.app == 'true'
    runs-on: ubuntu-latest

    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v6

      - name: Node.jsのインストール
        uses: actions/setup-node@v6
        with:
          node-version: 24.15.0

      - name: Corepackの有効化
        run: corepack enable

      - name: Corepackでpnpmのインストール
        run: corepack prepare pnpm@10.33.2 --activate

      - name: pnpmの「store path」を取得
        id: pnpm-store
        run: echo "STORE_PATH=$(pnpm store path --silent)" &gt;&gt; $GITHUB_OUTPUT

      - name: pnpmのキャッシュを取得
        uses: actions/cache@v5
        with:
          path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-

      - name: .lockから依存関係をインストール
        run: pnpm install --frozen-lockfile

      - name: テスト「unit/integration」の実行
        run: pnpm test

  # e2e tests
  e2e-test:
    needs: changes
    if: github.ref == 'refs/heads/main' &amp;&amp; needs.changes.outputs.app == 'true'
    runs-on: ubuntu-latest

    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v6

      - name: Node.jsのインストール
        uses: actions/setup-node@v6
        with:
          node-version: 24.15.0

      - name: Corepackの有効化
        run: corepack enable

      - name: Corepackでpnpmのインストール
        run: corepack prepare pnpm@10.33.2 --activate

      - name: pnpmの「store path」を取得
        id: pnpm-store
        run: echo "STORE_PATH=$(pnpm store path --silent)" &gt;&gt; $GITHUB_OUTPUT

      - name: pnpmのキャッシュを取得
        uses: actions/cache@v5
        with:
          path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-

      - name: .lockから依存関係をインストール
        run: pnpm install --frozen-lockfile

      - name: playwrightのブラウザをインストール
        run: pnpm exec playwright install --with-deps

      - name: テスト「e2e」の実行
        run: pnpm test:e2e</code></pre>
</div>
<p><span style="color: #ff0000;">※起動条件はプルリクエスト（PR）作成時か、mainブランチへのプッシュやマージ実行時です。加えて対象ファイルの差分変更を検知して各種処理を実行させているのと、e2eテストはmainブランチへのマージ時のみ実行させています。また、「actions/checkout@v6」や「dorny/paths-filter@v4」はnodeのバージョンが上がった際にバージョンを上げる必要がでる可能性があります。</span></p>
<p>&nbsp;</p>
<p>これでPR作成時やmainブランチにマージするタイミングでCIが実行されます。</p>
<p>もし<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>CIをスキップしたい場合</strong></span>は、<strong></strong><span style="background-color: #ffc6d1;"><strong>コミットメッセージの先頭に「[skip ci]」または「[ci skip]」を付ける</strong></span>とスキップ処理が可能です。</p>
<p>&nbsp;</p>
<h2>作成したプロジェクトをGitHubのリポジトリに登録</h2>
<p>次に上記で作成したプロジェクトをGitHubのリポジトリに登録します。</p>
<p>まずは以下のコマンドを実行し、コミット処理まで完了して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ git add -A
$ git commit -m "add init"</code></pre>
</div>
<p>&nbsp;</p>
<p>コミット完了後、GitHubのリポジトリ一覧画面にある「New」から新規リポジトリを作成し、表示される実行用コマンドを使ってプロジェクトをリポジトリにプッシュして下さい。</p>
<p>プッシュ後、プロジェクトが反映され、かつGitHub Actionsも起動します。GitHub Actionsの詳細を確認したい場合は、メニュー「Actions」から可能です。</p>
<p>&nbsp;</p>
<h3>mainブランチなどの保護設定をしたい場合</h3>
<p>実務などでは<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>mainブランチの保護設定</strong></span>が必要になったりしますが、その場合はリポジトリのメニューから「Settings」画面を開き、画面左のメニュー「Branches」を選択後、Branch protection rules画面が開くので、「Add branch ruleset」をクリックします。</p>
<p>次にRuleset Nameを入力後、Enforcement statusを「Active」に変更し、Target branchesの項目にある「Add target」から「Include by pattern」を選択します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18.jpg" alt="" width="2726" height="1590" class="aligncenter size-full wp-image-20353" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18.jpg 2726w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-300x175.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-1024x597.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-768x448.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-1536x896.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-2048x1195.jpg 2048w" sizes="auto, (max-width: 2726px) 100vw, 2726px" />
<p>&nbsp;</p>
<p>次にポップアップが表示されるので、「main」を入力して「Add Inclusion pattern」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19.jpg" alt="" width="2722" height="1584" class="aligncenter size-full wp-image-20354" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19.jpg 2722w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-300x175.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-1024x596.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-768x447.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-1536x894.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-2048x1192.jpg 2048w" sizes="auto, (max-width: 2722px) 100vw, 2722px" />
<p>&nbsp;</p>
<p>これでTarget branchesの項目に対象のブランチ名「main」が設定されます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20.jpg" alt="" width="2724" height="1576" class="aligncenter size-full wp-image-20355" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20.jpg 2724w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-1024x592.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-768x444.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-1536x889.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-2048x1185.jpg 2048w" sizes="auto, (max-width: 2724px) 100vw, 2724px" />
<p>&nbsp;</p>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>マージする前にプルリクエストを必須にしたい場合は、「Require a pull request before merging」のチェック</strong></span>を付け、<span style="color: #ff0000;">必要に応じてオプションを設定</span>して下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21.jpg" alt="" width="2732" height="1582" class="aligncenter size-full wp-image-20356" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21.jpg 2732w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-1024x593.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-768x445.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-1536x889.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-2048x1186.jpg 2048w" sizes="auto, (max-width: 2732px) 100vw, 2732px" />
<p>&nbsp;</p>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>CIをPASS（正常終了）することを必須にしたい場合は、「Require status checks to pass」のチェック</strong></span>を付け、<span style="color: #ff0000;">必要に応じてオプションを設定</span>し、<strong></strong><span style="background-color: #ffc6d1;"><strong>「Add checks」のリストからCIの対象のジョブを検索してチェック</strong></span>を付けて下さい。</p>
<p><span style="color: #ff0000;">※今回は「check」と「unit-test」のみチェックをつけます。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22.jpg" alt="" width="2732" height="1578" class="aligncenter size-full wp-image-20357" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22.jpg 2732w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-300x173.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-1024x591.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-768x444.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-1536x887.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-2048x1183.jpg 2048w" sizes="auto, (max-width: 2732px) 100vw, 2732px" />
<p>&nbsp;</p>
<p>全ての設定完了後、画面下の「Create」をクリックします。</p>
<p>次に認証を求められるので、認証をして下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25.jpg" alt="" width="2716" height="1564" class="aligncenter size-full wp-image-20360" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25.jpg 2716w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-300x173.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-1024x590.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-768x442.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-1536x885.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-2048x1179.jpg 2048w" sizes="auto, (max-width: 2716px) 100vw, 2716px" />
<p><span style="color: #ff0000;">※私はパスキー認証にしています。</span></p>
<p>&nbsp;</p>
<p>これでブランチ保護設定が完了です。今回は一例ですが、他にも色々条件が付けられるので、必要に応じて任意のものを設定して下さい。</p>
<p>&nbsp;</p>
<p><script src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" async=""></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>OpenAI「Codexアプリ」で開発する</h2>
<p>次にAIツールにはOpenAI「Codexアプリ」を利用して開発していきます。</p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/openai-codex-cli-app-guide" target="_blank" rel="noopener">・OpenAI Codex CLI / Codexアプリの使い方【ChatGPT時代のAI開発ツール入門】</a></p>
<p><a href="https://tomoyuki65.com/rails-ai-tdd-ddd-modular-monolith" target="_blank" rel="noopener">・Ruby on Railsで始めるAI駆動開発×TDD実践｜Codexとハーネスエンジニアリングで作るDDDモジュラーモノリス</a></p>
<p>&nbsp;</p>
<p>そして、Codexアプリを利用した開発の主な流れは以下の通りです。</p>
<div class="supplement boader">
<ol>
<li> プランモードで開発計画を立てる（必要に応じてスキル「plan-to-issue」を使ってGitHub Issueを自動登録）</li>
<li> 通常モードでタスク実行</li>
<li> タスク完了後にレビュー（必要に応じて修正を繰り返す）</li>
<li> レビュー完了後にスキル「auto-commit」を使って自動コミット</li>
<li> スキル「auto-pr」を使って自動PR作成</li>
<li> PRレビュー（必要に応じて修正作業）</li>
<li> PRレビュー完了後にmainにマージ（対象のIssueがあればクローズ）</li>
</ol>
</div>
<p>&nbsp;</p>
<h3>コンポーネントの配置場所について</h3>
<p>コンポーネントに関する各種ファイルを作成する際のディレクトリ構成については、一例になりますが、私は以下のような感じで作ってますので、よければ参考にどうぞ。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-9.png" alt="" width="760" height="906" class="aligncenter wp-image-20521" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-9.png 712w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-9-252x300.png 252w" sizes="auto, (max-width: 760px) 100vw, 760px" />
<p><span style="color: #ff0000;">※尚、Next.jsでは、サーバーコンポーネントとクライアントコンポーネントがありますが、特に理由がなければサーバーコンポーネントで作ります。また、src/app/page.tsxなどのページファイルは作成したコンポーネントを配置して作るようにし、薄く作った方がいいです。</span></p>
<p>&nbsp;</p>
<h3>SDDとTDDについて</h3>
<p>今回はSDD（Storybook Driven Development）とTDD（Test Driven Development）を採用していますが、<span style="color: #ff0000;"><strong>テスト駆動開発の場合はまずUI用のストーリーファイル「.stories.tsx」や、振る舞いがある場合はテストコード「.test.tsx」を最初に作り、それをまずしっかりレビューし、レビュー完了後に実装を進めた方がいい</strong></span>です。</p>
<p>ただし、フロントエンド開発であればバックエンド開発に比べて責任範囲が狭いため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>コンポーネント開発などで重要な部分がない場合においては、実装部分のコンポーネントもまとめて作っても大丈夫</strong></span>です。</p>
<p>&nbsp;</p>
<h3>AI駆動開発におけるトレードオフ関係について</h3>
<p>AI駆動開発では、<strong></strong><span style="background-color: #ffc6d1;"><strong>できるだけ人間がコードの中身を見ない、レビューもできるだけしないというのが重要（ハーネス設計が必須）</strong></span>になり、それによって<strong><span style="color: #3366ff;">爆発的な開発スピードを実現</span></strong>できるようになります。</p>
<p>ただし、<span style="color: #ff0000;"><strong>それに伴って品質に課題が出る可能性がある</strong></span>ため、<strong></strong>ここは<span style="border-bottom: 2px solid #be3144;"><strong>スピードを取るか、品質を取るかのトレードオフ関係</strong></span>があります。</p>
<p>フロントエンド開発ではスピード重視で問題ないケースが多いと思いますが、<strong><span style="color: #ff0000;">バックエンド開発などではスピードよりも品質が最重視されることが多いのでその点は注意が必要</span></strong>です。</p>
<p>その場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>TDDを採用するならしっかりテストコードをレビューして作り込み、その上で実装をし、必要に応じてパフォーマンスの観点等でリファクタリングするのが大事</strong></span>になります。</p>
<p>&nbsp;</p>
<h3>開発例：GitHubリポジトリを検索できるアプリケーション</h3>
<p>今回は例として、GitHubリポジトリを検索できるアプリケーションを開発しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-5.png" alt="" width="2560" height="1844" class="aligncenter size-full wp-image-20515" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-5.png 2560w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-5-300x216.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-5-1024x738.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-5-768x553.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-5-1536x1106.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-5-2048x1475.png 2048w" sizes="auto, (max-width: 2560px) 100vw, 2560px" />
<p>&nbsp;</p>
<p>リポジトリを検索すると一覧で表示されるので、対象のリポジトリをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-6.png" alt="" width="2558" height="1846" class="aligncenter size-full wp-image-20516" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-6.png 2558w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-6-300x216.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-6-1024x739.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-6-768x554.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-6-1536x1108.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-6-2048x1478.png 2048w" sizes="auto, (max-width: 2558px) 100vw, 2558px" />
<p><span style="color: #ff0000;">※リポジトリの検索については、GitHubのパブリックAPIの「https://api.github.com/search/repositories」を利用しています。</span></p>
<p>&nbsp;</p>
<p>クリック後、リポジトリの詳細が確認できます。</p>
<p>画面左上のタイトルロゴをクリックするとトップページ画面に戻れます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-7.png" alt="" width="2562" height="1838" class="aligncenter size-full wp-image-20517" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-7.png 2562w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-7-300x215.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-7-1024x735.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-7-768x551.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-7-1536x1102.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-7-2048x1469.png 2048w" sizes="auto, (max-width: 2562px) 100vw, 2562px" />
<p><span style="color: #ff0000;">※リポジトリの詳細情報取得については、GitHubのパブリックAPIの「https://api.github.com/repos/${owner}/${repo}」を利用しています。</span></p>
<p>&nbsp;</p>
<p>トップページ画面に戻っても元の検索結果が表示されるようにしています。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-8.png" alt="" width="2562" height="1842" class="aligncenter size-full wp-image-20518" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-8.png 2562w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-8-300x216.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-8-1024x736.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-8-768x552.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-8-1536x1104.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-8-2048x1472.png 2048w" sizes="auto, (max-width: 2562px) 100vw, 2562px" />
<p><span style="color: #ff0000;">※キャッシュ関連ではReact向けのデータ取得・キャッシュ管理ライブラリ「swr」を利用しています。</span></p>
<p>&nbsp;</p>
<p>尚、今回私が試した際の作業時間のサマリについては、以下のような結果になりました。</p>
<table style="border-collapse: collapse; width: 100%;">
<tbody>
<tr>
<td style="width: 50%; background-color: #ff9500; text-align: left;"><strong>タスク</strong></td>
<td style="width: 50%; background-color: #ff9500; text-align: left;"><strong>作業時間</strong></td>
</tr>
<tr>
<td style="width: 50%; text-align: left;">開発環境の初期セットアップ</td>
<td style="width: 50%; text-align: left;">約10時間</td>
</tr>
<tr>
<td style="width: 50%; text-align: left;">ハーネス設計</td>
<td style="width: 50%; text-align: left;">約10時間</td>
</tr>
<tr>
<td style="width: 50%; text-align: left;">AIツールのプランモードによるタスク分解・Issue登録 <span style="color: #ff0000;">※全12Issue登録</span></td>
<td style="width: 50%; text-align: left;">約10時間</td>
</tr>
<tr>
<td style="width: 50%; text-align: left;">AI駆動開発(タスク実行)</td>
<td style="width: 50%; text-align: left;">約10時間</td>
</tr>
<tr>
<td style="width: 50%; text-align: left;"><strong>合計</strong></td>
<td style="width: 50%; text-align: left;"><strong>約40時間</strong></td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p><strong><span style="color: #ff0000;">タスクを実行する前の準備等で約30時間</span></strong>かかりましたが、<strong></strong><span style="background-color: #ffc6d1;"><strong>タスク実行部分については全12Issue（通常だと最低でも12営業日ぐらいかかる量）を約10時間で完了</strong></span>できたため、単純に計算しても<span style="font-size: 14pt;"><strong><span style="color: #3366ff;">約10倍の開発効率アップを実現</span></strong></span>できました！</p>
<p><span style="color: #ff0000;">※12日 × 8時間 ÷ 10時間 = 9.6 ≒ 10</span></p>
<p>&nbsp;</p>
<p><span style="font-size: 24pt;"><strong>凄い！！</strong></span></p>
<p>&nbsp;</p>
<h3>e2eテストも作って試す</h3>
<p>コンポーネントを作る際は必要に応じて（振る舞いがある場合）「.test.tsx」を作り、src/app/page.tsxなどのページファイルを作った際は、必要に応じて「tests/integration」配下にintegrationテスト用の「.test.tsx」を作ります。</p>
<p>そして、<strong></strong><span style="background-color: #ffc6d1;"><strong>ユーザーのユースケース（システムを使って目的を達成する具体的な「利用場面」や「一連の手順」のこと）を満たせるような画面や機能が揃ったら、「tests/e2e」配下にe2eテスト用の「.spec.ts」</strong></span>を作ります。</p>
<p>今回はStorybookの導入の際に合わせてe2eテスト用のPlaywrightも導入しているため、e2eテストを作った際は、以下のコマンドを使ってe2eテストを実行できます。</p>
<p>・e2eテスト実行（UIなし）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm test:e2e</code></pre>
</div>
<p>&nbsp;</p>
<p>・e2eテスト実行（UIあり）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ pnpm test:e2e:ui</code></pre>
</div>
<p>&nbsp;</p>
<p>特に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>UIありのe2eテストを実行</strong></span>すると、以下のように<span style="color: #3366ff;"><strong>画面でデバッグなどもできる</strong></span>ため、有効活用して下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-10.jpg" alt="" width="2548" height="1578" class="aligncenter size-full wp-image-20522" srcset="https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-10.jpg 2548w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-10-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-10-1024x634.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-10-768x476.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-10-1536x951.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/05/next16-aidd-10-2048x1268.jpg 2048w" sizes="auto, (max-width: 2548px) 100vw, 2548px" />
<p><span style="color: #ff0000;">※尚、e2eテストはユースケース単位で薄く作ります。外部APIを利用している場合は、「.spec.ts」ファイル内で「await page.route()」を利用してモック化もできます。</span></p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はNext.jsによるAI駆動開発の実践方法についてご紹介しました。</p>
<p>実際に試してみて<span style="font-size: 14pt;"><strong><span style="color: #3366ff;">開発効率が約10倍</span></strong></span>になった点も踏まえ、Next.jsのフロントエンド開発についてもAI駆動開発に対応できました。</p>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>これからのシステム開発ではAI駆動開発が必須になっていく</strong></span>ので、興味がある方はぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/substack"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2026/07/substack-start-3-486x290.jpg" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">Substackを始めました！注目のニュースレター（メルマガ）＆SNSサービス</p><span class="date gf">2026年7月2日</span></div></a></div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/nextjs-v16-ai-driven-frontend-development">Next.js v16でAI駆動開発を実践！モダンフロントエンド開発におけるSDD・TDD・Codex・ハーネスエンジニアリング</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/nextjs-v16-ai-driven-frontend-development/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Ruby on Railsで始めるAI駆動開発×TDD実践｜Codexとハーネスエンジニアリングで作るDDDモジュラーモノリス</title>
		<link>https://tomoyuki65.com/rails-ai-tdd-ddd-modular-monolith</link>
					<comments>https://tomoyuki65.com/rails-ai-tdd-ddd-modular-monolith#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Fri, 24 Apr 2026 12:27:14 +0000</pubDate>
				<category><![CDATA[4. プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20251</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 2026年現在、IT業界は生成AIの渦に飲まれ、システム開発におけるコーディングにおいては「人が書くもの」から「AIに書かせるもの」へ急速に移行...</p>
The post <a href="https://tomoyuki65.com/rails-ai-tdd-ddd-modular-monolith">Ruby on Railsで始めるAI駆動開発×TDD実践｜Codexとハーネスエンジニアリングで作るDDDモジュラーモノリス</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-0.jpg" alt="" width="672" height="480" class="aligncenter size-full wp-image-20465" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-0.jpg 672w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-0-300x214.jpg 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>2026年現在、IT業界は生成AIの渦に飲まれ、<span style="color: #ff0000;"><strong>システム開発におけるコーディングにおいては「人が書くもの」から「AIに書かせるもの」へ急速に移行</strong></span>しています。</p>
<p>もう既に<strong><span style="color: #ff0000;">生成AIを使った開発は避けられない</span></strong>ため、<strong></strong>エンジニアの仕事をしている方は適宜キャッチアップし、生成AIを使った開発手法として<strong>『<span style="border-bottom: 2px solid #be3144;">AI駆動開発</span>』</strong>を使いこなす必要があります。</p>
<p>そんなAI駆動開発において、<strong></strong><span style="background-color: #ffc6d1;"><strong>次に覇権を取りそう</strong></span>だなと思っているプログラミング言語のフレームワークとして<strong>「<span style="border-bottom: 2px solid #be3144;">Ruby on Rails</span>」</strong>があります。</p>
<p>その理由としては、<strong></strong><span style="background-color: #ffc6d1;"><strong>Ruby on Railsの設計思想は「設定より規約」</strong></span>となっており、ディレクトリ構成やコードの書き方、ベストプラクティスがほぼ標準化されていて、<span style="color: #3366ff;"><strong>実装パターンの揺れが小さく、生成AIが前提として学習・再現しやすい構造になっている</strong></span>からです。</p>
<p>ということで、私もRuby on RailsでAI駆動開発を試してみることにしました。</p>
<p>この記事では、そんな私が試したRuby on RailsによるAI駆動開発の実践方法についてご紹介します。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Ruby on Railsで始めるAI駆動開発×TDD実践｜Codexとハーネスエンジニアリングで作るDDDモジュラーモノリス</h2>
<p>まず今回はmacOS（Apple silicon）のPCを使い、パッケージ管理に「<a href="https://brew.sh/ja/" target="_blank" rel="noopener">Homebrew</a>」、コード管理に「Git」や「<a href="https://github.com" target="_blank" rel="noopener">GitHub</a>」、ローカル開発環境に「<a href="https://www.docker.com/ja-jp/get-started/" target="_blank" rel="noopener">Docker</a>」、AIツールはOpenAIの「<a href="https://openai.com/ja-JP/codex/" target="_blank" rel="noopener">Codex</a>」を利用する前提で進めます。</p>
<p>以降の内容を参考にしたい場合は、事前にこれらを利用できるように準備して下さい。</p>
<p><span style="color: #ff0000;">※GitHub CLIとして「gh」コマンドも準備が必要です。これはHomebrewでインストールできます。</span></p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/openai-codex-cli-app-guide" target="_blank" rel="noopener">・OpenAI Codex CLI / Codexアプリの使い方【ChatGPT時代のAI開発ツール入門】</a></p>
<p>&nbsp;</p>
<h3>Ruby on Railsのローカル開発環境構築</h3>
<p>次にRuby on Railsのローカル開発環境を構築します。</p>
<p>まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir rails-aidd &amp;&amp; cd rails-aidd
$ mkdir -p deploy/docker/local/db &amp;&amp; touch deploy/docker/local/db/Dockerfile
$ mkdir -p deploy/docker/local/ruby &amp;&amp; touch deploy/docker/local/ruby/Dockerfile
$ mkdir src &amp;&amp; touch src/Gemfile src/Gemfile.lock
$ touch .env compose.yml .gitignore</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「deploy/docker/local/db/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/db/Dockerfile"><code>FROM postgres:18.3

ENV LANG ja_JP.utf8

# PostgreSQLの日本語化で「ja_JP.utf8」を使うために必要
RUN apt-get update &amp;&amp; \
    apt-get install -y locales &amp;&amp; \
    rm -rf /var/lib/apt/lists/* &amp;&amp; \
    localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8</code></pre>
</div>
<p><span style="color: #ff0000;">※DBはPostgreSQLのバージョンは「18.3」を使います。</span></p>
<p>&nbsp;</p>
<p>・「deploy/docker/local/ruby/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/ruby/Dockerfile"><code>FROM ruby:4.0.2-slim

RUN apt-get update -qq &amp;&amp; \
    apt-get install -y --no-install-recommends \
      locales \
      build-essential \
      git \
      libpq-dev \
      libyaml-dev &amp;&amp; \
    rm -rf /var/lib/apt/lists/*

RUN echo "ja_JP.UTF-8 UTF-8" &gt; /etc/locale.gen &amp;&amp; \
    locale-gen
WORKDIR /src

COPY src/Gemfile src/Gemfile.lock .

# bundlerのバージョンは固定して実行
RUN gem install bundler -v 4.0.10
RUN bundle _4.0.10_ install

COPY ./src /src

EXPOSE 3000</code></pre>
</div>
<p><span style="color: #ff0000;">※Rubyのバージョンは「4.0.2」、bundlerのバージョンは「4.0.10」を使います。</span></p>
<p>&nbsp;</p>
<p>・「src/Gemfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/Gemfile"><code>source 'https://rubygems.org'
gem 'rails', '8.1.3'</code></pre>
</div>
<p><span style="color: #ff0000;">※Ruby on Railsのバージョンは「8.1.3」を使います。</span></p>
<p>&nbsp;</p>
<p>・「.env」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".env"><code>ENV=local
RAILS_ENV=development
TZ=Asia/Tokyo
LANG=ja_JP.UTF-8
LC_ALL=ja_JP.UTF-8
PORT=3000
DB_HOST=db
DB_USER=root
DB_PASSWORD=root</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はこのような環境変数を使い、それを.envを使って利用しますが、このような機密情報を扱う可能性があるものについては、ちゃんと使い方を理解してから使うようにして下さい。特にこれからのAI駆動開発では、AIツールから見える範囲にAPIキーなどの機密情報を置くべきではない（.envだろうが、1Password CLIだろうが、根本原因の解決にならず、プロンプトインジェクションの可能性を防げない）ので、APIキーを使う必要がある場合などは、そもそもAIツールを使わずに開発するなどして下さい。</span></p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/generative-ai-security-secrets-zero-trust-variables" target="_blank" rel="noopener">・生成AIで機密情報は大丈夫？AIツール開発のセキュリティ対策｜ゼロトラスト・サンドボックス・環境変数（.env）管理</a></p>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  app:
    container_name: rails-app
    build:
      context: .
      dockerfile: ./deploy/docker/local/ruby/Dockerfile
    command: bash -c "rm -f tmp/pids/server.pid &amp;&amp; bundle exec rails s -b 0.0.0.0 -p 3000"
    volumes:
      - ./src:/src
    ports:
      - "3000:3000"
    env_file:
      - .env
    tty: true
    stdin_open: true
    depends_on:
      db:
        condition: service_healthy
  db:
    container_name: rails-db
    build:
      context: .
      dockerfile: ./deploy/docker/local/db/Dockerfile
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      TZ: ${TZ}
      # ローカル環境でもパスワードを有効化する設定
      POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256
    volumes:
      - rails-db-data:/var/lib/postgresql
    ports:
      - "5432:5432"
    env_file:
      - .env
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 5s
      timeout: 5s
      retries: 5
volumes:
  rails-db-data:</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.gitignore」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".gitignore"><code>.DS_Store
.env</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナをビルドします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Railsのプロジェクトを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm app rails new . -d postgresql \
--css=tailwind \
--skip-action-mailer \
--skip-action-mailbox \
--skip-action-text \
--skip-active-storage \
--skip-action-cable \
--skip-jbuilder \
--skip-test \
--skip-system-test</code></pre>
</div>
<p><span style="color: #ff0000;">※DBにPostgreSQLを使うのでオプション「-d postgresql」を付与、CSSにTailwind CSSを使うのでオプション「&#8211;css=tailwind」を付与、そしてできるだけ最小構成でやるため各種スキップオプションを付与してます。</span></p>
<p>&nbsp;</p>
<p>実行後、ファイル「src/Gemfile」を上書きしていいか聞かれるので、「y」を入力して実行します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-1.jpg" alt="" width="1116" height="650" class="aligncenter size-full wp-image-20261" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-1.jpg 1116w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-1-300x175.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-1-1024x596.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-1-768x447.jpg 768w" sizes="auto, (max-width: 1116px) 100vw, 1116px" />
<p>&nbsp;</p>
<p>上記処理ではGitの初期化処理はスキップせず、.gitignoreを合わせて作成するようにしていますが、Git管理は不要なので、以下のコマンドを実行してGit管理を削除します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm app rm -r .git</code></pre>
</div>
<p>&nbsp;</p>
<p>全て完了後、以下のようにRailsのプロジェクトとして各種ファイルが作成されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-2.png" alt="" width="2192" height="1462" class="aligncenter size-full wp-image-20262" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-2.png 2192w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-2-300x200.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-2-1024x683.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-2-768x512.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-2-1536x1024.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-2-2048x1366.png 2048w" sizes="auto, (max-width: 2192px) 100vw, 2192px" />
<p>&nbsp;</p>
<p>次にファイル「src/Gemfile」に対して以下のように追加のライブラリを記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/Gemfile"><code>・・・

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

  # Audits gems for known security defects (use config/bundler-audit.yml to ignore issues)
  gem "bundler-audit", require: false

  # Static analysis for security vulnerabilities [https://brakemanscanner.org/]
  gem "brakeman", require: false

  # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
  gem "rubocop-rails-omakase", require: false

  # ERB用のlint
  gem "erb_lint", require: false

  # テストライブラリ
  gem "rspec-rails", require: false

  # テストデータ用
  gem "factory_bot_rails", require: false
end

・・・

# テスト用
group :test do
  gem "faker"
  gem "capybara"
  gem "selenium-webdriver"
  gem "webdrivers"
  gem "shoulda-matchers", require: false
end</code></pre>
</div>
<p><span style="color: #ff0000;">※「group :development, :test do」の中に、「gem &#8220;erb_lint&#8221;, require: false」、「gem &#8220;rspec-rails&#8221;, require: false」、「gem &#8220;factory_bot_rails&#8221;, require: false」を追加してます。そしてテスト用として「group :test do」でテスト用の各種gemを追加してます。</span></p>
<p>&nbsp;</p>
<p>次にファイル「src/config/application.rb」に対して、以下のようにタイムゾーン設定を追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="src/config/application.rb"><code>・・・

module Src
  class Application &lt; Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 8.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks])

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US &amp; Canada)"
    # config.eager_load_paths &lt;&lt; Rails.root.join("extras")

    # Don't generate system test files.
    config.generators.system_tests = nil

    # タイムゾーン設定
    config.time_zone = ENV.fetch("TZ", "UTC")
  end
end</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「src/config/database.yml」に対して、以下のようにdefault設定にDB接続設定を追加、production設定のusernameとpasswordはコメントアウトします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/config/database.yml"><code>・・・

default: &amp;default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  max_connections: &lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %&gt;
  # DB接続設定を追加
  host: &lt;%= ENV.fetch("DB_HOST") { "localhost" } %&gt;
  username: &lt;%= ENV.fetch("DB_USER") { "postgres" } %&gt;
  password: &lt;%= ENV.fetch("DB_PASSWORD") { "password" } %&gt;

・・・

production:
  primary: &amp;primary_production
  &lt;&lt;: *default
  database: src_production
  # コメントアウト
  # username: src
  # password: &lt;%= ENV["SRC_DATABASE_PASSWORD"] %&gt;

・・・</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナを再ビルドします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナの起動を確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose ps</code></pre>
</div>
<p>&nbsp;</p>
<p><span>コマンド実行後、appコンテナとdbコンテナが起動すればOKです。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-3.png" alt="" width="1686" height="124" class="aligncenter size-full wp-image-20267" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-3.png 1686w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-3-300x22.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-3-1024x75.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-3-768x56.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-3-1536x113.png 1536w" sizes="auto, (max-width: 1686px) 100vw, 1686px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、DBを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec app rails db:create</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:3000」にアクセスし、以下のようにエラーにならずに正常にトップページが表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-4.png" alt="" width="2734" height="1830" class="aligncenter size-full wp-image-20269" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-4.png 2734w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-4-300x200.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-4-1024x685.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-4-768x514.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-4-1536x1028.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-4-2048x1371.png 2048w" sizes="auto, (max-width: 2734px) 100vw, 2734px" />
<p>&nbsp;</p>
<h3>テストフレームワーク「RSpec」の導入</h3>
<p>次に以下のコマンドを実行し、Railsで人気のテストフレームワーク「RSpec」を導入していきます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec app rails g rspec:install</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成された設定ファイル「src/.rspec」について、「<span style="color: #ff0000;"><code>--color</code></span>」と「<code>--format documentation</code>」を追加します。</p>
<p>・「src/.rspec」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="src/.rspec"><code>--require spec_helper
--color
--format documentation</code></pre>
</div>
<p>&nbsp;</p>
<p>次にファイル「src/config/application.rb」に対して、以下のようにジェネレーター設定を追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="src/config/application.rb"><code>・・・

module Src
  class Application &lt; Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 8.1

    # Please, add to the `ignore` list any other `lib` subdirectories that do
    # not contain `.rb` files, or that should not be reloaded or eager loaded.
    # Common ones are `templates`, `generators`, or `middleware`, for example.
    config.autoload_lib(ignore: %w[assets tasks])

    # Configuration for the application, engines, and railties goes here.
    #
    # These settings can be overridden in specific environments using the files
    # in config/environments, which are processed later.
    #
    # config.time_zone = "Central Time (US &amp; Canada)"
    # config.eager_load_paths &lt;&lt; Rails.root.join("extras")

    # Don't generate system test files.
    config.generators.system_tests = nil

    # タイムゾーン設定
    config.time_zone = ENV.fetch("TZ", "UTC")

    # ジェネレーター設定
    config.generators do |g|
      g.test_framework :rspec
      g.fixture_replacement :factory_bot, dir: "spec/factories"
      g.view_specs false
      g.helper_specs false
      g.routing_specs false
      g.controller_specs false
      g.system_tests nil
    end
  end
end</code></pre>
</div>
<p><span style="color: #ff0000;">※コントローラーファイルなどを作成時に不要なファイルを作らないようにする設定を追加してます。</span></p>
<p>&nbsp;</p>
<p>次にファイル「src/spec/rails_helper.rb」のENV[&#8216;RAILS_ENV&#8217;]の設定について以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="src/spec/rails_helper.rb"><code># This file is copied to spec/ when you run 'rails generate rspec:install'
require 'spec_helper'
# RSpec実行時の環境変数「RAILS_ENV」の値が「test」になるように修正
# ENV['RAILS_ENV'] ||= 'test'
ENV['RAILS_ENV'] = 'test'

・・・</code></pre>
</div>
<p><span style="color: #ff0000;">※RSpec実行時に「RAILS_ENV=test」で実行できるように修正</span></p>
<p>&nbsp;</p>
<p>次にファイル「src/spec/rails_helper.rb」において、まず「require &#8220;factory_bot_rails&#8221;」を追加後、RSpec.configureの部分でfixtureの設定をコメントアウトし、FactoryBotの設定を追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="src/spec/rails_helper.rb"><code>・・・

require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!

# FactoryBot追加
require "factory_bot_rails"

RSpec.configure do |config|
  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  # fixtureの設定をコメントアウト
  # config.fixture_paths = [
  #   Rails.root.join('spec/fixtures')
  # ]

  # FactoryBotの設定を追加
  config.include FactoryBot::Syntax::Methods
・・・</code></pre>
</div>
<p><span style="color: #ff0000;">※テストデータ作成が必要な際はFactoryBotを使う。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、appコンテナを再起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose restart app</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、RSpecの実行を試します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec app bundle exec rspec</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-5.png" alt="" width="946" height="168" class="aligncenter size-full wp-image-20277" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-5.png 946w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-5-300x53.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-5-768x136.png 768w" sizes="auto, (max-width: 946px) 100vw, 946px" />
<p>&nbsp;</p>
<h3>shoulda-matchersとcapybaraの設定追加</h3>
<p>次に「shoulda-matchers」（モデルのバリデーションチェック用）と「capybara」（ブラウザの画面テスト用）を使えるようにするため、ファイル「src/spec/rails_helper.rb」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby" data-file="src/spec/rails_helper.rb"><code>・・・

require 'rspec/rails'
# Add additional requires below this line. Rails is not loaded until this point!

# FactoryBot追加
require "factory_bot_rails"

# shoulda-matchers追加
require "shoulda/matchers"

# capybara追加
require 'capybara/rspec'

・・・

RSpec.configure do |config|

〜

end

・・・

# shoulda-matchers設定追加
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

# capybara設定
Capybara.default_driver = :selenium_chrome_headless</code></pre>
</div>
<p><span style="color: #ff0000;">※「require &#8220;shoulda/matchers&#8221;」と「require &#8216;capybara/rspec&#8217;」を追加、「Shoulda::Matchers.configure do |config| 〜 end」を追加、「Capybara.default_driver = :selenium_chrome_headless」を追加</span></p>
<p>&nbsp;</p>
<h3>「rubocop」でフォーマッターと静的コード解析を試す</h3>
<p>Railsにおけるフォーマッターと静的コード解析用として「rubocop」を入れているため、それぞれ試します。</p>
<p>まずは以下のコマンドを実行し、フォーマッターを試します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm app bundle exec rubocop -A --only Layout</code></pre>
</div>
<p><span style="color: #ff0000;">※オプション「-A」は自動修正、オプション「&#8211;only Layout」はレイアウトのみ対象</span></p>
<p>&nbsp;</p>
<p>実行後、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-6.png" alt="" width="1200" height="256" class="aligncenter size-full wp-image-20278" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-6.png 1200w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-6-300x64.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-6-1024x218.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-6-768x164.png 768w" sizes="auto, (max-width: 1200px) 100vw, 1200px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、静的コード解析を試します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm app bundle exec rubocop</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-7.png" alt="" width="992" height="258" class="aligncenter size-full wp-image-20279" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-7.png 992w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-7-300x78.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-7-768x200.png 768w" sizes="auto, (max-width: 992px) 100vw, 992px" />
<p>&nbsp;</p>
<h3>「erb_lint」の設定を追加してビュー用の静的コード解析を試す</h3>
<p>Railsにおけるビューの静的コード解析用として「erb_lint」を入れているため、設定を追加して試します。</p>
<p>まずは以下のコマンドを実行し、設定ファイル「src/.erb_lint.yml」を追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch src/.erb_lint.yml</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを次のように記述します。</p>
<p>・「src/.erb_lint.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git"><code>linters:
  ErbSafety:
    enabled: true

  FinalNewline:
    enabled: true

  TrailingWhitespace:
    enabled: true</code></pre>
</div>
<p><span style="color: #ff0000;">※ErbSafetyは危険なERBコードを検出、FinalNewlineはファイルの最後に改行があるかチェック、TrailingWhitespaceは行末の余計なスペースを検出です。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ビューの静的コード解析を試します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm app bundle exec erb_lint app/views</code></pre>
</div>
<p>&nbsp;</p>
<p>実行後、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-8.png" alt="" width="1442" height="280" class="aligncenter size-full wp-image-20280" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-8.png 1442w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-8-300x58.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-8-1024x199.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-8-768x149.png 768w" sizes="auto, (max-width: 1442px) 100vw, 1442px" />
<p><span style="color: #ff0000;">※今回はRubyの最新バージョンを利用しているため、警告が出ていますが無視して下さい。</span></p>
<p>&nbsp;</p>
<h3>「brakeman」でRailsの脆弱性検査を試す</h3>
<p>次に以下のコマンドを実行し、「brakeman」によるRailsの脆弱性検査を試します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec app bundle exec brakeman --no-pager</code></pre>
</div>
<p><span style="color: #ff0000;">※デフォルトではlessページャー（lessコマンド）を使って結果を出力するようになっており、このままだとCIに組み込んだりできない（実行後に止めるにはEnterを押す必要）ため、オプション「&#8211;no-pager」を使って結果をターミナルに直接出力するようにします。</span></p>
<p>&nbsp;</p>
<p>実行後、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-10.jpg" alt="" width="3004" height="848" class="aligncenter size-full wp-image-20311" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-10.jpg 3004w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-10-300x85.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-10-1024x289.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-10-768x217.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-10-1536x434.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-10-2048x578.jpg 2048w" sizes="auto, (max-width: 3004px) 100vw, 3004px" />
<p>&nbsp;</p>
<h3>Git管理</h3>
<p>次に以下のコマンドを実行し、Git管理できるようにします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ git init
$ git add -A
$ git commit -m "init"</code></pre>
</div>
<p>&nbsp;</p>
<h3>Gitフック管理ツール「Lefthook」を導入（任意）</h3>
<p>次に<span style="color: #ff0000;"><strong>チーム開発向け（任意）</strong></span>になりますが、Go言語製のGitフック管理ツール「Lefthook」を導入し、フォーマッターや静的コード解析、そしてテスト実行を自動化させます。</p>
<p>ツールはHomebrewで入れるため、以下のコマンドを実行してインストールして下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ brew install lefthook</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、lefthookを導入します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ lefthook install</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成されたファイル「lefthook.yml」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="lefthook.yml"><code>pre-commit:
  parallel: true
  commands:
    rubocop_format:
      run: docker compose run --rm app bundle exec rubocop -A --only Layout
    rubocop_lint:
      run: docker compose run --rm app bundle exec rubocop
    erb_lint:
      run: docker compose run --rm app bundle exec erb_lint app/views
    brakeman:
      run: docker compose run --rm app bundle exec brakeman --no-pager

pre-push:
  commands:
    rspec:
      run: |
        docker compose up -d
        docker compose exec -T app rails db:prepare
        docker compose exec -T app bundle exec rspec</code></pre>
</div>
<p><span style="color: #ff0000;">※pre-commit設定にコミット時に実行させたい処理、pre-push設定にプッシュ時に実行させたい処理を設定します。</span></p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/golang-lefthook-pre-commit" target="_blank" rel="noopener">・Go言語（Golang）開発でLefthookの使い方｜pre-commitでformat・lint（静的解析）を自動化</a></p>
<p>&nbsp;</p>
<h3>ログ出力にリクエスト単位の一意のID（リクエストID）を出力する方法</h3>
<p>実務などでは障害調査やデバッグのしやすくするため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ログ出力にはリクエスト単位の一意のID（リクエストID）を出力</strong></span>するようにしたりしますが、<strong><span style="color: #3366ff;">Railsであれば設定を追加することで簡単に実装可能</span></strong>です。</p>
<p>設定したい場合は、「src/config/environments」配下にある環境にあわせた<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>設定ファイル（開発用ならdevelopment.rb、本番環境ならproduction.rb）に「config.log_tags = [ :request_id ]」を追加</strong></span>して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-ruby" data-lang="Ruby"><code>require "active_support/core_ext/integer/time"


Rails.application.configure do


・・・


# ログ出力にリクエストIDを追加
config.log_tags = [ :request_id ]

end</code></pre>
</div>
<p>&nbsp;</p>
<p>設定後にDockerコンテナを再起動すると設定が反映され、ログ出力を試すと以下のようにログの先頭に一意のID（リクエストID）が付与されます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-27.jpg" alt="" width="2712" height="206" class="aligncenter size-full wp-image-20366" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-27.jpg 2712w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-27-300x23.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-27-1024x78.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-27-768x58.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-27-1536x117.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-27-2048x156.jpg 2048w" sizes="auto, (max-width: 2712px) 100vw, 2712px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>OpenAI「Codex」におけるハーネスエンジニアリング設定</h2>
<p>AI駆動開発をする際には何らかのAIツールを利用しますが、<span style="color: #ff0000;"><strong>今回はOpenAIの「Codexアプリ」または「Codex CLI」を利用する前提</strong></span>としています。</p>
<p>そしてAIツールを上手く使って開発するためには<strong><span style="background-color: #ffc6d1;">『ハーネスエンジニアリング』が重要</span></strong>になっており、<strong><span style="color: #ff0000;">事前にしっかりハーネス設計をする必要</span></strong>があります。</p>
<p>&nbsp;</p>
<h3>ハーネスエンジニアリングとは？</h3>
<p>AIツールにおける『ハーネスエンジニアリング』とは、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>AIモデルやAIエージェントを安全かつ安定して動作させるため、入力・出力の制御やルール設定、評価・テストの仕組みなどを設計・構築する技術</strong></span>のことです。</p>
<p>これによってAIの挙動を管理し、<span style="color: #3366ff;"><strong>品質や再現性を担保した状態で実運用</strong></span>できるようにします。</p>
<p>&nbsp;</p>
<h3>OpenAI「Codex」におけるハーネス設計について</h3>
<p>OpenAI「Codex」でハーネス設計をしたい場合は、以下のようなディレクトリ構成で各種ファイルを作成し、それぞれ内容を定義していくことになります。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>/my-project
 ├── /.codex
 |    ├── config.toml  ※codexに関する設定
 |    |
 |    ├── /rules  ※sandbox外で実行できるコマンド制御の設定
 |    |    └── default.rules
 |    |
 |    ├── /agents  ※サブエージェントを利用する場合の例
 |    |    ├── orchestrator.toml  ※指揮者（全体制御）
 |    |    ├── tester.toml        ※テスター（テストコード作成・検証）
 |    |    ├── implementer.toml   ※実装者（テストコードを通すように実装）
 |    |    └── reviewer.toml      ※レビュワー（設計や品質チェック）
 |    |
 |    ├── /workflows  ※ワークフローを利用する場合の例
 |    |    └── tdd_flow.md  ※TDD（テスト駆動開発）のフロー
 |    |
 |    └── /skills  ※Agent Skillsを利用する場合
 |
 ├── AGENTS.md  ※ルートディレクトリ用（共通ルール）
 |
 ├── /src
 |    └── AGENTS.md  ※サブディレクトリ用（専用ルール）
 |
 └── /docs  ※詳細仕様にファイル分割して格納（各種AGENTS.mdから指定する）
      └── /rules
           ├── architecture.md  ※設計思想のルール定義
           ├── database.md      ※DB設計のルール定義
           ├── sub.md           ※サブディレクトリ用のルール定義
           └── testing.md       ※テスト用のルール定義
 </code></pre>
</div>
<p>&nbsp;</p>
<h3>codexの設定ファイルを作成</h3>
<p>まずはcodexの設定ファイルを作成するため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir .codex &amp;&amp; touch .codex/config.toml</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「.codex/config.toml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".codex/config.toml"><code>model = "gpt-5.2"
model_reasoning_effort = "medium"

sandbox_mode = "workspace-write"</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はモデルに「gpt-5.2」を指定（利用中のプランで使用可能なものを指定して下さい）、モデルの推論設定に「medium（バランス型）」を指定、サンドボックスモードに「workspace-write（書き込み許可）」を指定</span></p>
<p>&nbsp;</p>
<p>尚、<strong><span style="color: #ff0000;">Codexアプリなどで対象のプロジェクトに設定した「.codex/config.toml」を有効化するには、対象のプロジェクトを信頼するプロジェクトにする必要</span></strong>があります。</p>
<p>対象のプロジェクトを信頼するプロジェクトにしたい場合は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>グローバル設定の方のconfig.toml（~/.codex/config.toml）で、以下のような設定を追加</strong></span>して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="~/.codex/config.toml"><code>[projects."対象のプロジェクトのフルパス"]
trust_level = "trusted"</code></pre>
</div>
<p><span style="color: #ff0000;">※設定変更を反映するにはCodexアプリの再起動が必要です。</span></p>
<p>&nbsp;</p>
<h3>sandbox外のコマンド制御設定を追加</h3>
<p>次にsandbox外で実行できるコマンドを制御するため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/rules &amp;&amp; touch .codex/rules/default.rules</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「.codex/rules/default.rules」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".codex/rules/default.rules"><code># --- ディスク破壊（即禁止） ---
prefix_rule(
    pattern=["mkfs", "dd if=", "wipefs"],
    decision="forbidden",
    justification="ストレージ破壊操作"
)

# --- Git履歴破壊（即禁止） ---
prefix_rule(
    pattern=["git push --force", "git push -f", "git push --force-with-lease"],
    decision="forbidden",
    justification="履歴改変（強制push）によるリポジトリ破壊の可能性"
)

# --- GitHub CLI 危険操作 ---
prefix_rule(
    pattern=["gh repo delete"],
    decision="forbidden",
    justification="リポジトリ削除（不可逆）"
)

prefix_rule(
    pattern=["gh pr merge --admin"],
    decision="prompt",
    justification="保護ルール無視の強制マージ"
)

prefix_rule(
    pattern=["gh workflow run"],
    decision="prompt",
    justification="CI/CDの強制実行"
)

prefix_rule(
    pattern=["gh secret set", "gh secret delete"],
    decision="prompt",
    justification="機密情報の変更"
)
# --- 危険削除（確認） ---
prefix_rule(
    pattern=["rm -rf", "sudo rm"],
    decision="prompt",
    justification="不可逆削除の可能性"
)

# --- 権限昇格 ---
prefix_rule(
    pattern=["sudo"],
    decision="prompt",
    justification="システム影響が大きい"
)</code></pre>
</div>
<p><span style="color: #ff0000;">※まずは危険なコマンドを制御する</span></p>
<p>&nbsp;</p>
<h3>agents（サブエージェント）機能を追加</h3>
<p>次にagents（サブエージェント）機能を試すため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/agents
$ touch .codex/agents/pm.toml .codex/agents/tester.toml .codex/agents/implementer.toml .codex/agents/reviewer.toml</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はpm（プロダクトマネージャー・指揮者）、tester（テスター）、implementer（実装者）、reviewer（レビュワー）の4名を想定しています。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「.codex/agents/pm.toml」（プロダクトマネージャー / 指揮者）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/agents/pm.toml"><code>name = "pm"
description = "全体の進行管理とタスク分解を行うプロダクトマネージャー"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたは開発全体を指揮するプロダクトマネージャーです。

役割：
- 要件を整理し、タスクに分解する
- tester → implementer → reviewer の順でタスクを割り振る
- 各エージェントのアウトプットを評価し、次の行動を決める

ルール：
- 必ずTDDサイクル（RED → GREEN → REVIEW）を守る
- 不完全な実装は次に進めない
- 問題があれば前の工程に差し戻す

出力形式：
- 次に行動するエージェント名
- 依頼内容（具体的に）
"""</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/agents/tester.toml」（テスター）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/agents/tester.toml"><code>name = "tester"
description = "テストコード作成と動作検証を行うTDDエンジニア"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたはTDDに従うテストエンジニアです。

役割：
- 仕様に基づいたテストコードを書く（RED）
- 実装後にテストを実行し検証する（GREEN）

ルール：
- まず失敗するテストを書く（RED）
- テストは仕様を正確に表現すること
- エッジケースも考慮する
- 実装コードは書かない

出力：
- テストコード
- テストの意図説明
"""</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/agents/implementer.toml」（実装者）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/agents/implementer.toml"><code>name = "implementer"
description = "テストを通すための実装を行うエンジニア"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"

developer_instructions = """
あなたはTDDに従う実装エンジニアです。

役割：
- テストが通る最小限の実装を書く

ルール：
- テストを通すことを最優先
- 過剰な設計をしない（YAGNI）
- テストを書かない
- リファクタは必要最低限

出力：
- 実装コード
- 実装の簡単な説明
"""</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/agents/reviewer.toml」（レビュワー）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/agents/reviewer.toml"><code>name = "reviewer"
description = "仕様適合性・品質・セキュリティをレビューするエンジニア"
model_reasoning_effort = "high"
sandbox_mode = "read-only"

developer_instructions = """
あなたはコードレビュー担当です。

役割：
- 実装が仕様を満たしているか確認
- バグ・セキュリティ問題を検出
- テスト不足を指摘

観点：
- 正しさ（仕様通りか）
- セキュリティ
- 境界値・異常系
- テスト網羅性

ルール：
- 指摘は具体的に
- 再現手順を書く
- スタイル指摘は重要な場合のみ

出力：
- 指摘一覧（重要度付き）
- 修正提案
"""</code></pre>
</div>
<p>&nbsp;</p>
<h3>workflows（ワークフロー）機能を追加</h3>
<p>次にworkflows（ワークフロー）機能も試すため、以下のコマンドを実行してファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/workflows &amp;&amp; touch .codex/workflows/tdd_flow.md</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はTDD開発を前提とします。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「.codex/workflows/tdd_flow.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/workflows/tdd_flow.md"><code># TDD開発フロー

本フローは pm の指示を起点として進行するが、
各エージェントは自身の責務に従い独立して実行する。

---
1. pmが要件を整理し、タスクとして分解する
2. testerが失敗するテストを書く（RED）
3. implementerがテストを通す（GREEN）
4. testerがテスト結果を検証する
5. reviewerがレビューを行う
6. 問題があれば修正ループに戻る

---

## ルール

- REDなしで実装しない
- GREEN未達でレビューしない
- レビューNGなら再実装</code></pre>
</div>
<p>&nbsp;</p>
<h3>Agent Skillsを追加</h3>
<p>次にAgent Skillsを試すため、以下のスキルを追加します。</p>
<p>&nbsp;</p>
<h4>・スキル「plan-to-issue」：プランモードで作成した開発計画をGitHubのIssue用のフォーマットへ変換して自動登録する</h4>
<p>まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/skills/plan-to-issue &amp;&amp; touch .codex/skills/plan-to-issue/SKILL.md
$ mkdir -p .codex/skills/plan-to-issue/references &amp;&amp; touch .codex/skills/plan-to-issue/references/rules.md
$ mkdir -p .codex/skills/plan-to-issue/assets &amp;&amp; touch .codex/skills/plan-to-issue/assets/template.md
$ mkdir -p .codex/skills/plan-to-issue/scripts &amp;&amp; touch .codex/skills/plan-to-issue/scripts/create_issue.sh
$ chmod +x .codex/skills/plan-to-issue/scripts/create_issue.sh</code></pre>
</div>
<p><span style="color: #ff0000;">※スクリプトは「chmod +x」で実行権限を付与します。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「.codex/skills/plan-to-issue/SKILL.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/plan-to-issue/SKILL.md"><code>---
name: plan-to-issue
description: プランモードで作成した開発計画をGitHubのIssue用のフォーマットへ変換して自動登録する
---

# plan-to-issue

## 概要
このスキルは、プランモードで作成された開発計画を読み込み、指定されたルールとテンプレートに従ってGitHub Issue用のフォーマットへ変換し、専用のスクリプトを用いてGitHubへ自動登録します。

## 参照ファイル
このスキルを実行する際は、以下のファイルを必ず読み込んで使用してください。

- **変換ルール定義**: `references/rules.md`
- 開発計画からIssueフォーマットへ変換する際のルールや抽出条件が記載されています。
- **出力フォーマット**: `assets/template.md`
- Issueの本文を作成するためのMarkdownテンプレートです。
- **実行用スクリプト**: `scripts/create_issue.sh`
- 作成したIssueデータをGitHubに登録するためのシェルスクリプトです。

## 入力
プランモードで生成された開発計画

## 出力
GitHub Issue（gh issue create により自動登録）

## 実行手順（ワークフロー）
以下のステップに従って処理を実行してください。

1. **開発計画の読み込み**
- 現在のプランモードで作成・合意された開発計画（タスク、目的、要件など）を確認します。

2. **変換ルール定義の読み込み**
- `references/rules.md` を読み込み、開発計画の内容をGitHub Issue用のフォーマットに変換するためのルールを理解します。

3. **出力フォーマットの読み込み、テンプレートの生成**
- `assets/template.md` を読み込み、開発計画の内容を変換ルールに基づいてテンプレートの構造に当てはめ、Issueの本文を生成します。

4. **Issueのタイトルを作成**
- 生成したIssueの本文の内容から、変換ルールに基づいてIssueのタイトルを作成します。

5. **GitHubのIssueを登録**
- `scripts/create_issue.sh` を利用し、作成したIssueのタイトルと本文をGitHubのIssueに登録します。</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/plan-to-issue/references/rules.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/plan-to-issue/references/rules.md"><code># 変換ルール定義（plan-to-issue）

このドキュメントは、プランモードで作成された開発計画をGitHub Issue用フォーマットへ変換するためのルールを定義する。

---

## 1. 基本方針

- 開発計画の内容を「1 Issue = 1目的（または1成果物）」の粒度で整理する
- 曖昧な記述は補完し、実行可能な形に正規化する
- 情報が不足している場合は、合理的に補完する（推測可）
- 冗長な説明は要約するが、重要な前提は保持する

---

## 2. フィールド対応ルール

開発計画の内容を以下のIssueテンプレートにマッピングする

### 2.1 概要（summary）
- 開発計画の「目的」「やること」を1〜3文で要約
- 実装内容ではなく「何を達成するか」を中心に記述
- 可能であれば動詞で開始する（例: 「〇〇を実装する」）

---

### 2.2 背景（background）
- なぜこの作業が必要なのかを記述
- 以下の情報を優先的に抽出
- 課題
- 現状の問題点
- ビジネス的/技術的な理由
- 明示されていない場合は、文脈から補完する

---

### 2.3 スコープ（scope）
- 対象範囲を明確化
- 含めるもの（In Scope）と含めないもの（Out of Scope）を整理
- 不明な場合は「暗黙のスコープ」を明文化する

例:
- 含む: API実装、UI変更
- 含まない: インフラ構築、リファクタリング全般

---

### 2.4 タスク（tasks）
- 実装手順をチェックリスト形式に分解
- 1タスク = 1アクション
- 粒度は「1〜3時間程度で完了できる単位」を目安に分割

#### タスク分解ルール
- 「設計」「実装」「テスト」「確認」を含める
- 抽象的な表現は禁止（例: 「いい感じに実装する」）
- 動詞で開始する（例: 「APIエンドポイントを作成する」）

#### 例
- [ ] APIのエンドポイントを定義する
- [ ] データモデルを作成する
- [ ] バリデーションを実装する
- [ ] 単体テストを追加する

---

### 2.5 受け入れ条件（acceptance_criteria）
- 完了とみなす条件を明確にする
- テスト可能・検証可能であることが必須

#### ルール
- 「〜できること」で記述
- 可能な限り具体的に書く
- 曖昧な表現は禁止（例: 「正常に動く」）

#### 例
- 正しいリクエストで200レスポンスが返ること
- 不正な入力時にバリデーションエラーが返ること
- UI上で新機能が操作できること

---

### 2.6 補足（notes）
- 以下の情報を含める
- 技術的な注意点
- 依存関係
- 未確定事項
- 将来対応

---

## 3. タイトル生成ルール

Issueタイトルは以下のルールで生成する

### フォーマット

[種別] 内容の要約

### 種別の分類
- feat: ユーザーに価値を提供する新機能
- fix: 不具合の修正
- refactor: 挙動を変えない内部改善
- docs: ドキュメントの追加・更新
- test: テストの追加・修正
- infra: インフラ・CI/CD・環境構築
- chore: 上記に当てはまらない雑務（極力使わない）

### ルール
- 30〜60文字程度に収める
- 内容が一目で分かるようにする
- summaryの内容をベースに生成する

例:
- feat: ユーザー登録APIを実装する
- fix: ログイン時の認証エラーを修正

---

## 4. Issue分割ルール

以下の場合は複数Issueに分割する

- 明確に独立した機能が複数存在する
- フロントエンドとバックエンドが分離可能
- 実装規模が大きすぎる（タスクが10個以上）

### 分割基準
- 各Issueが独立して完了可能であること
- 依存関係がある場合はnotesに記載

---

## 5. 不足情報の補完ルール

情報が不足している場合は以下に従う

- 文脈から合理的に推測する
- 推測した内容はnotesに明記する
- 重要な不確定要素は「要確認」として記載

---

## 6. 禁止事項

- 曖昧な表現（例: 適切に、いい感じに）
- 実行不可能なタスク分解
- 背景なしのIssue作成
- 受け入れ条件が検証不能な状態

---

## 7. 出力品質チェック

出力前に以下を確認する

- [ ] summaryが目的ベースになっている
- [ ] tasksが具体的で分解されている
- [ ] acceptance criteriaが検証可能である
- [ ] scopeが明確である
- [ ] タイトルが簡潔で分かりやすい</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/plan-to-issue/assets/template.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/plan-to-issue/assets/template.md"><code>## 概要
{{summary}}

## 背景
{{background}}

## スコープ
{{scope}}

## タスク
- [ ] {{tasks}}

## 受け入れ条件
- {{acceptance_criteria}}

## 補足
{{notes}}</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/plan-to-issue/scripts/create_issue.sh」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash" data-file=".codex/skills/plan-to-issue/scripts/create_issue.sh"><code>#!/usr/bin/env bash

set -euo pipefail

# ===============================
# Usage:
# ./create_issue.sh "タイトル" "本文"
# ===============================

TITLE="${1:-}"
BODY="${2:-}"

# タイトルと本文の入力チェック
if [[ -z "$TITLE" || -z "$BODY" ]]; then
  echo "Usage: $0 \"タイトル\" \"本文\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &amp;&gt; /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &amp;&gt; /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "Issueを作成中..."

# 一時ファイル作成
TMP_FILE=$(mktemp)

# 一時ファイルをスクリプト終了時に削除
trap 'rm -f "$TMP_FILE"' EXIT

# 本文を一時ファイルに書き込み
printf "%s" "$BODY" &gt; "$TMP_FILE"

# Issue作成
gh issue create \
  --title "$TITLE" \
  --body-file "$TMP_FILE"

echo "Issue作成完了"
</code></pre>
</div>
<p>&nbsp;</p>
<h4>・スキル「auto-commit」：修正したコードをステージングしたうえで差分を解析し、適切なコミットメッセージを生成してgit commitまでを自動で実行する</h4>
<p>次に以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/skills/auto-commit &amp;&amp; touch .codex/skills/auto-commit/SKILL.md</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「.codex/skills/auto-commit/SKILL.md」</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/auto-commit/SKILL.md"><code>---
name: auto-commit
description: <span>修正したコードをステージングしたうえで差分を解析し、適切なコミットメッセージを生成してgit commitまでを自動で実行する</span>
---

# auto-commit

## 概要
このスキルは、コード修正後の未ステージ状態から変更を検出し、自動でステージング、差分解析、コミットメッセージ生成、git commit実行までを一貫して行う。

## 処理フロー

### 1. 未ステージの変更をステージ

以下のコマンドを実行し、未ステージの変更をすべてステージする。

```bash
git add -A
```

---

### 2. ステージ済み差分の取得

以下のコマンドを実行し、ステージ済みの変更内容の差分情報を取得する。

```bash
git diff --cached
```

---

### 3. 差分解析

取得したコードの差分情報から以下を解析する：

- 変更の目的（機能追加 / バグ修正 / リファクタリング / 雑務）
- 影響範囲（ファイル・モジュール）
- ユーザー視点での変化
- 変更が単一責務かどうか

---

### 4. コミットメッセージ生成

コードの差分情報の解析結果をもとに、
Conventional Commits形式でコミットメッセージを生成する。

#### フォーマット

`&lt;type&gt;: &lt;summary&gt;`

#### type一覧
- feat: 新規機能追加
- fix: バグ修正
- refactor: リファクタリング・性能改善
- test: テストの追加・修正
- docs: ドキュメント修正
- chore: その他

#### コミットメッセージの例

fix: ログイン時のトークン更新処理の不具合を修正

---

### 5. コミット実行

生成したコミットメッセージを用いて以下のコマンドを実行し、コミットを実行する。

```bash
git commit -m "&lt;generated commit message&gt;"
```

---

## 動作ルール

### 0. 実行順序

- 処理は必ず「git add → diff確認 → commit可否判定」の順で実行する
- git add は処理の最初に一度だけ実行する（追加実行は禁止）

---

### 1. 変更検知・終了条件

- 変更が存在しない場合は、git add / commit は実行せず処理を終了する
- git diff --cached の結果が空の場合も同様にコミットを行わない
- git add 後に再度差分を確認し、変更がない場合は即終了する
- 空コミットは絶対に作成しない

---

### 2. 安全性チェック（危険変更の制御）

- .env, secrets, credential系ファイルの変更が含まれる場合は必ず停止する
- 破壊的変更（大量削除・ファイル削除が多数）の場合は警告を出し、停止する

- 破壊的変更の定義：
- 削除ファイルが5件以上
- または差分行数の削除が追加の2倍以上

- 上記条件を満たす場合は自動実行せず、必ず停止してユーザー確認を求める

---

### 3. コミット構造ルール

- 1つのコミットは必ず単一の目的（単一責務）になるようにする
- 複数の変更目的が混在している場合は論理的にまとめるか警告扱いとする

- 大規模変更（複数モジュールにまたがる変更）の場合は、可能であれば分割コミットを優先する

---

### 4. コミットメッセージ生成ルール

- コミットメッセージは必ず変更内容に基づいて生成し、推測や一般論で補完しない
- 差分から読み取れない情報は含めない

- メッセージは簡潔にしつつ、変更の「意図」が分かる表現にする
- 実装内容の羅列ではなく、何が改善されたかを優先する

- コミットメッセージは50〜72文字程度を目安にする

- 英語・日本語のどちらを使うかはプロジェクトの既存コミットに合わせて統一する

---

### 5. 変更分類ルール

- 変更内容が以下に該当する場合は chore に分類する
- フォーマット修正のみ
- コメント修正のみ
- 空白・改行整理のみ
- 自動生成ファイルの更新

---

### 6. コミット実行ルール

- commit 実行前に最終的なメッセージを内部で確定させること
- git commit は確定したメッセージのみで実行する

---

## まとめ

このスキルは以下を保証する：

- 安全な自動コミット（事故防止）
- 意味ベースのコミット生成
- 単一責務の維持
- 変更意図の明確化
- チーム規約との整合性</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<h4>・スキル「auto-pr」：直前のコミット内容をもとにPRタイトルと本文を生成し、GitHubのPR用フォーマットへ変換して自動登録する</h4>
<p>次に以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .codex/skills/auto-pr &amp;&amp; touch .codex/skills/auto-pr/SKILL.md
$ mkdir -p .codex/skills/auto-pr/assets &amp;&amp; touch .codex/skills/auto-pr/assets/template.md
$ mkdir -p .codex/skills/auto-pr/scripts &amp;&amp; touch .codex/skills/auto-pr/scripts/create_pr.sh
$ chmod +x .codex/skills/auto-pr/scripts/create_pr.sh</code></pre>
</div>
<p><span style="color: #ff0000;">※スクリプトは「chmod +x」で実行権限を付与します。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「.codex/skills/auto-pr/SKILL.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/auto-pr/SKILL.md"><code>---
name: auto-pr
description: 直前のコミット内容をもとにPRタイトルと本文を生成し、GitHubのPR用フォーマットへ変換して自動登録する
---

# auto-pr

## 概要

このスキルは、直前のコミット内容をもとにPRタイトルと本文を生成し、GitHubのPR用フォーマットへ変換して自動登録します。

## 参照ファイル

このスキルを実行する際は、以下のファイルを必ず読み込んで使用してください。

- PRテンプレート: `assets/template.md`
- PR作成スクリプト: `scripts/create_pr.sh`

## 入力

直前のコミット情報

## 出力

GitHubのPR（gh pr create により自動登録）

---

## 実行手順

### 1. 直前のコミット情報と変更内容を取得する

```bash
git log -1 --pretty=format:"%h%n%s%n%b"
git show --no-color
```

- git log: 意図（タイトル・背景）
- git show: 変更内容（diff）

---

### 2. 解析して以下を生成する

- summary（概要）
- background（背景・目的）
- implementation（実装内容：箇条書き）
- test_items（テスト確認項目：チェックリスト形式）
- impact（影響範囲：箇条書き）
- future_work（未対応・今後の課題：箇条書き）

---

### 3. PR本文を生成する

`assets/template.md` を読み込み、以下ルールで置換する：

- {{summary}} → summary
- {{background}} → background
- {{implementation}} → implementation
- {{test_items}} → test_items
- {{impact}} → impact
- {{future_work}} → future_work

---

### 4. PRタイトルを生成する

フォーマット：

```text
auto: &lt;summaryの短縮版&gt;
```

---

### 5. PRを作成する

`scripts/create_pr.sh` を利用し、生成したPRタイトルと本文をGitHubのプルリクエストに登録します。

---

## 補足ルール

- implementation / impact / future_work は必ず箇条書きで出力する
- test_items はチェックリスト形式（- [ ]）で出力する
- diff（git show）を必ず参照し、推測のみで生成しない
</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/auto-pr/assets/template.md」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file=".codex/skills/auto-pr/assets/template.md"><code>## 概要
{{summary}}

## 背景・目的
{{background}}

## 実装内容
- {{implementation}}

## テスト確認項目
- [ ] {{test_items}}

## 影響範囲
- {{impact}}

## 未対応・今後の課題
- {{future_work}}</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.codex/skills/auto-pr/scripts/create_pr.sh」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash" data-file=".codex/skills/auto-pr/scripts/create_pr.sh"><code>#!/usr/bin/env bash

set -euo pipefail

# ===============================
# Usage:
# ./create_pr.sh "タイトル" "本文"
# ===============================

TITLE="${1:-}"
BODY="${2:-}"

# タイトルと本文の入力チェック
if [[ -z "$TITLE" || -z "$BODY" ]]; then
  echo "Usage: $0 \"タイトル\" \"本文\""
  exit 1
fi

# gh コマンド存在チェック
if ! command -v gh &amp;&gt; /dev/null; then
  echo "gh コマンドが見つかりません"
  exit 1
fi

# GitHub認証チェック
if ! gh auth status &amp;&gt; /dev/null; then
  echo "GitHubにログインしていません"
  echo "gh auth login を実行してください"
  exit 1
fi

echo "PRを作成中..."

# 一時ファイル作成
TMP_FILE=$(mktemp)

# 一時ファイルをスクリプト終了時に削除
trap 'rm -f "$TMP_FILE"' EXIT

# 本文を一時ファイルに書き込み
printf "%s" "$BODY" &gt; "$TMP_FILE"

# PR作成
gh pr create \
  --title "$TITLE" \
  --body-file "$TMP_FILE"

echo "PR作成完了"</code></pre>
</div>
<p>&nbsp;</p>
<h3>今回のプロジェクト用のハーネス設定</h3>
<p>次に今回のプロジェクト用のハーネス設定を行いますが、<strong></strong><span style="background-color: #ffc6d1;"><strong>今回はRailsでMVCのアプリかつ、将来性を考慮してモジュラーモノリス（DDD）設計にすることを想定</strong></span>しています。</p>
<p>そのため、今回のRailsアプリのディレクトリ構成としては、以下のような構成を想定します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>/src
 └── /app
      ├── /controllers（コントローラー定義）
      |    |
      |    ├── /web
      |    |
      |    └── /api （将来的に利用する想定のAPIモード用）
      |
      ├── /models（アプリ全体の共通データ層・ActiveRecordモデル置き場）
      |
      ├── /views（Rails用のビュー）
      |
      ├── /domains（中核の業務領域・ドメイン駆動設計）
      |    |
      |    └── /[domain_name]
      |         |
      |         ├── /domain（ドメイン層）
      |         |    |
      |         |    ├── /entities（エンティティ）
      |         |    |
      |         |    ├── /value_objects（値オブジェクト）
      |         |    |
      |         |    ├── /services（ドメインサービス）
      |         |    |
      |         |    └── /repositories（リポジトリのインターフェース）
      |         |
      |         ├── /use_cases（ユースケース層）
      |         |
      |         └── /infrastructure（インフラストラクチャ層）
      |              |
      |              └── /repositories（リポジトリの実装）
      |                   |
      |                   ├── /command（書き込み）
      |                   |
      |                   └── /query（読み込み）
      |
      ├── /features（補完的な業務領域・トランザクションスクリプト）
      |    |
      |    └── /[feature_name]
      |         |
      |         └── /services（サービス層） 
      |
      ├── /resources（一般的な業務領域・アクティブレコード）
      |     ※例：user.rbやpayment_gateway.rbなどを置く
      |
      └── /shared（横断関心）</code></pre>
</div>
<p><span style="color: #ff0000;">※今回は使っていませんが、実際には「/app/container」ディレクトリなどを作ってユースケースにリポジトリをDIできるようにし、それをコントローラーから呼び出すようにした方がいいです。</span></p>
<p>&nbsp;</p>
<p>このように、<span style="background-color: #ffc6d1;"><strong>今回の例ではモジュラーモノリスかつドメイン駆動設計（DDD）を考慮したディレクトリ構成</strong></span>としています。</p>
<p>モジュラーモノリスは、システム全体は1つのデプロイ単位（モノリス）として扱いながら、分割されたモジュール（機能群）で構成する設計手法です。</p>
<p>ドメイン駆動設計（DDD：Domain-Driven Design）は複雑な業務ロジックを中心に据え、共通言語（ユビキタス言語）を用いて設計する手法です。</p>
<p>特に<strong><span style="color: #ff0000;">ドメイン駆動設計については難しい</span></strong>ため、<strong><span style="border-bottom: 2px solid #be3144;">実務を想定している方については、以下の関連記事などを参考にしたり、事前にドメイン駆動設計の理解をするようにして下さい。</span></strong></p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/domain-driven-design-essentials" target="_blank" rel="noopener">・ドメイン駆動設計（Domain-Driven Design / DDD）の本質と大事なこと</a></p>
<p>&nbsp;</p>
<p>そして、今回は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>AI駆動開発</strong></span>を試していきますが、その際は<strong></strong><span style="background-color: #ffc6d1;"><strong>テスト駆動開発（TDD）も併せてしていく流れ</strong></span>になります。</p>
<p>テスト駆動開発（TDD：Test-Driven Development）については、プログラムの機能実装前に対応するテストコードを先に作成し、そのテストに合格するように実装とリファクタリングを繰り返す開発手法です。</p>
<p>今回はそれらを踏まえて各種定義ファイルを作成していく必要があるため、まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p docs/rules
$ touch docs/rules/architecture.md docs/rules/database.md
$ touch docs/rules/boundaries.md docs/rules/rails.md docs/rules/testing.md
$ touch src/AGENTS.md AGENTS.md</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はできるだけ最小構成になるようにしています。ファイル数を増やしてしっかり定義した方が、より想定外の動作やリスクを防ぐことができるようになります。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「docs/rules/architecture.md」（アーキテクチャ設計のルール定義）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="docs/rules/architecture.md"><code># アーキテクチャ設計のルール定義

## 概要
本プロジェクトはRailsベースのモジュラーモノリス構成かつドメイン駆動設計（DDD）を採用し、業務の複雑さと表現力に応じて以下の3層に分離します。

- 中核の業務領域 → ドメイン駆動設計（DDD）→ domains
- 補完的な業務領域 → トランザクションスクリプト → features
- 一般的な業務領域 → アクティブレコード（単純なCRUD） → resources

---

## ディレクトリ構造の前提

```
/src
 ├── /app
 |    ├── /controllers（コントローラー定義）
 |    |    |
 |    |    ├── /web
 |    |    |
 |    |    └── /api （将来的に利用する想定のAPIモード用）
 |    |
 |    ├── /models（アプリ全体の共通データ層・ActiveRecordモデル置き場）
 |    |
 |    ├── /views（Rails用のビュー）
 |    |
 |    ├── /domains（中核の業務領域・ドメイン駆動設計）
 |    |    |
 |    |    └── /[domain_name]
 |    |         |
 |    |         ├── /domain（ドメイン層）
 |    |         |    |
 |    |         |    ├── /entities（エンティティ）
 |    |         |    |
 |    |         |    ├── /value_objects（値オブジェクト）
 |    |         |    |
 |    |         |    ├── /services（ドメインサービス）
 |    |         |    |
 |    |         |    └── /repositories（リポジトリのインターフェース）
 |    |         |
 |    |         ├── /use_cases（ユースケース層）
 |    |         |
 |    |         └── /infrastructure（インフラストラクチャ層）
 |    |              |
 |    |              └── /repositories（リポジトリの実装）
 |    |                   |
 |    |                   ├── /command（書き込み）
 |    |                   |
 |    |                   └── /query（読み込み）
 |    |
 |    ├── /features（補完的な業務領域・トランザクションスクリプト）
 |    |    |
 |    |    └── /[feature_name]
 |    |         |
 |    |         └── /services（サービス層） 
 |    |
 |    ├── /resources（一般的な業務領域・アクティブレコード）
 |    |     ※例：user.rbやpayment_gateway.rbなどを置く
 |    |
 |    └── /shared（横断関心）
 |
 └── /spec（テストコード）
```

---

## レイヤー構成の定義

### domains（中核の業務領域 / ドメイン駆動設計）

事業の本質的価値を生み出す領域

#### 特徴

- 業務ルールが複雑で変化しやすい
- 意思決定ロジックの中心
- ビジネス競争力に直結する領域
- ドメインモデルが重要になる

#### 設計原則（DDD適用）

- 集約・エンティティ / 値オブジェクト / ドメインサービスを適用する
- ビジネスルールをコードとして明示的に表現する
- 副作用を最小化する
- 可能な限り純粋ロジックとして設計する

#### 構造

- [domain_name]
  - domain（ドメイン層）
    - entities（エンティティ）
    - value_objects（値オブジェクト）
    - services（ドメインサービス）
    - repositories（リポジトリのインターフェース）
  - use_cases（ユースケース層）
  - infrastructure（インフラストラクチャ層）
    - repositories（リポジトリの実装）
      - command（書き込み）
      - query（読み取り）

#### 例

- 与信判断
- 価格最適化
- 予約枠管理
- 配送ルート最適化

---

### features（補完的な業務領域 / トランザクションスクリプト）

中核業務を支える周辺処理領域

#### 特徴

- 業務ルールは存在するが比較的単純
- 処理の流れ（手続き）が中心
- 複数処理や外部処理の調整役

#### 設計原則

- トランザクションスクリプトとしてサービス層に実装する
- ユースケース単位で記述する
- トランザクション境界はサービス層で明確に管理する
- シンプルなデータ操作に限定し、複雑なロジックは持ち込まない
- 可読性を優先し、副作用や永続化は明示的に分離する
- 複雑化・肥大化した場合はdomainsへ移行する

#### 構造

- [feature_name]
  - services（サービス層）

#### 例

- 注文後メール送信
- ポイント付与
- 在庫減算処理
- 単純な承認フロー

---

### resources（一般的な業務領域 / アクティブレコード）

業務固有性が低いデータ管理領域

#### 特徴

- CRUD中心の処理
- ビジネスルールが薄い
- データの保存・取得が主目的

#### 設計原則

- ActiveRecordベースで十分表現可能
- インフラ寄りの扱い
- 業務判断を持たない

#### 構造

- [resource_name.rb]

#### 例

- ユーザー基本情報
- マスターデータ（カテゴリ・都道府県など）
- ログ
- 設定情報

---

## shared（横断関心）

全レイヤーから参照可能な共通処理

### 例

- /utils
- /constants
- /types
- /errors
- /validators
- /formatters
- /time
- /pagenation

---

## models（データ層 / ActiveRecord集約層）

- 全てのActiveRecordのモデルはここに集約する
- データアクセスの唯一の責務を持つ
- 業務ロジックは禁止
- 各レイヤーから利用される基盤層

---

## 依存関係

### 呼び出し方向（実行フロー）

- controller → domains/use_cases または features/services または resources
- domains/use_cases → repositories → models
- features/services → models
- resources → models
- shared （全レイヤーから参照可能）

---

## 基本原則

- controller：HTTPリクエストの受け取り、ユースケースの呼び出し専用
- domains：中核の業務領域、複雑で競争優位性が高いビジネスをここに集約
- features：補完的な業務領域、単純で競争優位性を生み出さないビジネスをここに集約
- resources：一般的な業務領域、複雑だが競争優位性を生み出さずに共通の仕組みを利用するビジネスをここに集約
- shared：横断関心、各レイヤーから利用可能な共通の処理を集約
- models：データ層

---

## 設計判断基準

設計する際の分類基準は以下の通り：

- ルールやビジネスロジックが複雑になり、破綻しやすいもの → domains
- ルールが薄く、単純であり、処理フロー中心のもの → features
- 単純なCRUD処理や、共通の外部サービスを利用するもの → resources

---

## その他の重要ルール

- controllerは直接modelsを呼び出して操作しない
- controllerは必ずユースケース層を経由する
- レイヤー内部の実装詳細は外部から呼ばない
</code></pre>
</div>
<p>&nbsp;</p>
<p>・「docs/rules/database.md」（データベース設計のルール定義）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="docs/rules/database.md"><code># データベース設計のルール定義

## 基本方針

- ActiveRecordを唯一のORMとして使用する
- DBは永続化の責務のみを持つ

---

## モデル設計

- 全モデルは src/app/models に集約する
- ビジネスロジックは禁止
- scopeは軽量クエリのみ許可

---

## スキーマ設計

- 破壊的変更を避ける
- NULL制約・外部キー制約を積極的に使用
- インデックスは用途ベースで設計する

---

## 禁止事項

- DBに業務ルールを持たせない
- fat model禁止</code></pre>
</div>
<p>&nbsp;</p>
<p>・「docs/rules/boundaries.md」（<span>コード配置のルール定義・境界ルール定義</span>）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="docs/rules/boundaries.md"><code># コード配置のルール定義・境界ルール定義

## 目的

業務重要度と役割に応じてコードの配置場所を決定する。

---

## ディレクトリ構造の前提

```
/src
 ├── /app
 |    ├── /controllers（コントローラー定義）
 |    |    |
 |    |    ├── /web
 |    |    |
 |    |    └── /api （将来的に利用する想定のAPIモード用）
 |    |
 |    ├── /models（アプリ全体の共通データ層・ActiveRecordモデル置き場）
 |    |
 |    ├── /views（Rails用のビュー）
 |    |
 |    ├── /domains（中核の業務領域・ドメイン駆動設計）
 |    |    |
 |    |    └── /[domain_name]
 |    |         |
 |    |         ├── /domain（ドメイン層）
 |    |         |    |
 |    |         |    ├── /entities（エンティティ）
 |    |         |    |
 |    |         |    ├── /value_objects（値オブジェクト）
 |    |         |    |
 |    |         |    ├── /services（ドメインサービス）
 |    |         |    |
 |    |         |    └── /repositories（リポジトリのインターフェース）
 |    |         |
 |    |         ├── /use_cases（ユースケース層）
 |    |         |
 |    |         └── /infrastructure（インフラストラクチャ層）
 |    |              |
 |    |              └── /repositories（リポジトリの実装）
 |    |                   |
 |    |                   ├── /command（書き込み）
 |    |                   |
 |    |                   └── /query（読み込み）
 |    |
 |    ├── /features（補完的な業務領域・トランザクションスクリプト）
 |    |    |
 |    |    └── /[feature_name]
 |    |         |
 |    |         └── /services（サービス層） 
 |    |
 |    ├── /resources（一般的な業務領域・アクティブレコード）
 |    |     ※例：user.rbやpayment_gateway.rbなどを置く
 |    |
 |    └── /shared（横断関心）
 |
 └── /spec（テストコード）
```

---

## レイヤー定義

### app/controllers（コントローラー）

- HTTPリクエストを受け取り、レスポンスを返すための薄いレイヤー
- パラメータの受け取り・バリデーション・認証/認可の入口を担う
- ユースケース層（app/domains/*/use_cases or app/features/*/services or app/resources/*）を呼び出す
- 業務ロジックは持たない（持ってはいけない）
- レスポンス整形（JSON / HTML）は行うが、ビジネス判断は行わない

#### 依存ルール

- controllers → ユースケース（app/domains/*/use_cases or app/features/*/services or app/resources/*）
- controllers → views（HTMLの場合）

---

### app/models（データ層 / ActiveRecordモデル置き場）

- ActiveRecordベースのDBテーブルとのマッピングを担う
- スキーマ表現・リレーション・スコープなどの永続化に近い関心を持つ
- コールバックやロジックは極力最小限に抑える
- ドメイン層から直接使わず、Infrastructure層経由で利用するのが基本
- 「Fat Model」にしない（ビジネスロジックはドメインへ）

#### 依存ルール

- models ← infrastructure（repositoryなど）

---

### app/views（Rails用のビュー）

- HTMLテンプレートの描画を担う
- 表示ロジック（フォーマット・簡単な条件分岐）のみ許容
- 業務ロジックやドメイン知識は持たない
- instance変数はcontrollerから受け取る

#### 依存ルール

- views ← controllers

---

### app/domains（中核の業務領域 / ドメイン駆動設計）

- アプリケーションの最も重要なビジネスロジックを集約
- 境界づけられたコンテキスト（= domain_name）単位で分割

#### domain/entities

- 同一性（ID）を持つオブジェクト
- ビジネスルール・不変条件を保持
- 永続化手段（ActiveRecordなど）に依存しない

#### domain/value_objects

- 不変オブジェクト
- 値による等価性で比較される
- ドメインルールを内包可能（例：金額、メールアドレス）

#### domain/services

- エンティティやVOに属さないドメインロジック
- 複数のエンティティにまたがるビジネスルールを表現

#### domain/repositories

- リポジトリのインターフェース定義
- 実装部分はinfrastructure/repositoriesで行う
- コマンド・クエリ責務分離（CQRS）設計とし、command（書き込み）とquery（読み取り）に分ける

#### use_cases

- アプリケーションのユースケース単位の処理
- トランザクション境界を管理
- ドメインオブジェクトを組み合わせて処理を実行
- 外部I/F（repositoryなど）はインターフェース経由で利用

#### infrastructure/repositories

- 永続化の具体実装
- ActiveRecord（app/models）を利用してDBアクセスを行う
- ドメイン層に定義されたRepositoryインターフェースを実装
- 外部APIや他システム連携もここに含めてよい

#### 依存ルール

- domain層は他のレイヤーに依存しない
- use_casesはdomainに依存するが、infrastructureの詳細には依存しない（DI前提）
- infrastructureはdomainに依存してよい

---

### app/features（補完的な業務領域 / トランザクションスクリプト）

- ドメインとして切り出すほどではないが、機能としてまとまりがある処理
- シンプルな業務ロジックを扱う
- サービスとして実装する

#### services

- 1サービス1ユースケースとして表現
- ActiveRecord（app/models）を直接利用してよい
- 複雑化したらdomainsへ昇格させる前提

#### 位置づけ

- 軽量なユースケース層
- DDDの厳密さを求めない代わりにスピードを優先

#### 依存ルール

- features → models はOK
- features → domains は原則避ける

---

### app/resources（一般的な業務領域 / アクティブレコード）

- 単純なCRUD処理など、共通の仕組みを利用するような処理

#### 例

- ActiveRecordベースのシンプルなCRUD処理
  - 例：User、PaymentGatewayなど
- 外部サービスのような共通の仕組みを利用する単純な処理
- ビジネスルールが薄い処理

#### 役割

- ドメインに昇格しないActiveRecordベースの共通処理置き場

---

### app/shared（横断関心）

- 全レイヤーから参照可能な共通処理

#### 例

- /utils
- /constants
- /types
- /errors
- /validators
- /formatters
- /time
- /pagenation

---

### spec（テストコード）

#### 基本方針

- レイヤーごとに責務に応じたテストを行う
- 下位レイヤーほど高速・純粋、上位レイヤーほど統合的に検証する
- 「どこで何を保証するか」を明確にし、テストの重複を避ける
- ビジネスロジックは domainsで担保する（他レイヤーで重複して検証しない）

---

#### 主な分類

- controllers：インテグレーションテスト → リクエストスペック
  - HTTPリクエスト/レスポンスの検証（ステータスコード、JSON構造など）
  - 認証・認可の動作確認
  - ユースケースが正しく呼ばれていることの間接確認
  - 業務ロジックの詳細までは検証しない
  - 可能な限りモックを使わず、実際のスタックに近い形で検証する

- models：ユニットテスト → モデルスペック
  - スコープ・バリデーション・リレーションの検証
  - DBに近い振る舞い（クエリ結果など）の確認
  - コールバックは必要最小限にし、テストも最小限に留める
  - ビジネスロジックはテストしない（domainで担保）

- views：E2Eテスト → システムスペック
  - ユーザー操作を通した画面表示の検証
  - フォーム入力〜結果表示までの一連のフロー確認
  - 表示崩れや重要なUI要素の存在確認
  - 細かい文言やHTML構造には依存しすぎない
  - JavaScriptを含む挙動もここで検証

- domains：ユニットテスト
  - ビジネスルール・不変条件の検証
  - エンティティ・ValueObject・ドメインサービスの振る舞い確認
  - 外部依存（DB・API）は排除し、純粋なオブジェクトとしてテスト
  - 境界値・異常系を重点的にテストする
  - 最も網羅的かつ信頼できるテスト層

- features：インテグレーションテスト
  - Request Specを利用
  - サービス単位でのユースケース検証
  - ActiveRecordを含めた一連の処理の動作確認
  - 複雑な分岐や副作用（レコード作成・更新）を検証
  - controller経由 or 直接サービス呼び出しのどちらでもよい
  - domainに昇格すべきロジックが混ざっていないかの検知にも使う

- resources：E2Eテスト
  - API：request spec（統合テスト） / View：system spec（E2Eテスト）
    - API：クライアント視点でリクエストの連携が成立することを担保
    - View：ユーザー操作として一連の体験が成立することを担保
  - 主要なフロー（登録 → 認証 → 操作）の検証
  - 認証・認可・決済など横断的関心の動作確認
  - クライアント視点でAPIが正しく連携できることを担保
  - 成功パターンを中心に最小限のケースに絞る
  - 詳細なバリデーションや分岐ロジックは下位レイヤーで担保

---

#### 補足ポリシー

- ドメイン層は最速で実行できるテストにする（DBアクセス禁止）
- インフラ層（repositoryなど）は必要に応じて統合テストを行う
- モックは「外部境界」に限定して使用する（DB・外部APIなど）
- テストが壊れやすい構造（過度なモック、内部実装依存）を避ける
- 迷った場合は「その責務はどのレイヤーか」に立ち返って配置する

---

## ユースケース呼び出しルール

- controllerは各レイヤーに定義されたユースケースのみ呼び出す。
- 内部実装やモデルは直接呼ばない。

### 例

- domains/use_casesのユースケース
- features/servicesのユースケース
- resourcesのユースケース

---

## 配置判断基準

- HTTP処理 → controllers
- ActiveRecordモデル（永続化） → models
- 画面表示 → views
- ビジネスルール（純粋ロジック） → domains
- ユースケースの流れ制御 → features
- DBに対する単純操作（ロジックなし） → resources
- 共通処理 → shared

---

## 禁止事項

- domainsにCRUDロジックを混ぜる
- controllerに業務ロジックを書く
- レイヤー内部を直接呼び出す</code></pre>
</div>
<p>&nbsp;</p>
<p>・「docs/rules/rails.md」（Rails実装のルール定義）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="docs/rules/rails.md"><code># Rails実装のルール定義

モジュラーモノリス + ドメイン駆動設計（DDD）を併用する

## 1. 目的

本ドキュメントはRailsにおける実装ルールを定義する。

アーキテクチャ判断は `architecture.md` と `boundaries.md` に従い、
本書は「Railsコードの書き方・制約」に特化する。

---

## 2. 基本原則

### 2-1. controllersは薄くする

- HTTP入出力のみ担当
- 業務ロジック禁止
- 必ずユースケース層を呼び出す

#### 許可される処理

- パラメータ取得
- 認証・認可の入口処理
- ユースケースの呼び出し
- レスポンス整形

#### 禁止

- 条件分岐による業務判断
- DB操作
- ドメインロジック

---

### 2-2. modelsは永続化専用

- ActiveRecordはデータ層として扱う
- ビジネスロジック禁止
- scope・validation・associationのみ許可

#### 禁止

- 業務ルール
- 計算ロジック
- ドメイン判断

---

### 2-3. domainsは純粋な業務ロジック

- ActiveRecordに依存しない
- 不変条件を保証する
- 副作用を外に出す

#### 特徴

- エンティティ
- 値オブジェクト
- ドメインサービス

---

### 2-4. domainsのuse_caseはアプリケーション制御

- トランザクション境界を持つ
- domainを組み合わせる
- repository経由でデータアクセス

#### 禁止

- UIロジック
- ActiveRecord直接操作（原則）

---

### 2-5. featuresは軽量ユースケース

- 単純な業務フローを扱う
- ActiveRecord利用OK
- 複雑化したらdomainへ移行

---

### 2-6. resourcesはCRUD専用

- 単純なデータ操作のみ
- ビジネス判断を持たない
- マスタ・設定・ログなど

---

## 3. 依存ルール

### 基本方針

依存は一方向のみ（上位 → 下位）

---

### 3-1. controllers

- controllers → domains/[domain_name]/use_cases
- controllers → features/[feature_name]/services
- controllers → resources

#### ルール

- controllerは業務判断を持たない
- HTTP入出力のみに責務を限定する
- domains/[domain_name] / features/[feature_name] / resourcesのどれを呼ぶかの判断のみ行う

---

### 3-2. models

- models（ActiveRecordのモデルを集約） → DB永続化専用

---

### 3-3. domains（中核の業務領域）

#### domain/*

- domains/*/domain → 無依存（完全独立）
- repositories
- domains/*/domain/repositories（interface）はインターフェース定義のみ
- 実装はdomains/*/infrastructure/repositories（implementation）

#### use_cases

- domains/*/use_cases → domains/*/domain
- domains/*/use_cases → domains/*/domain/repositories（interface）

#### infrastructure

- リポジトリの実装を行う
- domains/*/infrastructure/repositories（implementation） → models
- コマンド・クエリ責務分離（CQRS）設計とし、command（書き込み）とquery（読み取り）に分ける

---

### 3-3. features（補完的な業務領域）

- features/*/services → models

#### ルール

- 単純な業務フローのみ扱う
- 複雑化した場合はdomainsへ移行

---

### 3-4. resources（一般的な業務領域）

- resources → models

#### ルール

- 単純なCRUD処理
- ビジネスロジック禁止

---

### 3-5. shared（横断関心）

- shared → 全レイヤーから参照可能

---

### 3-6. 全体制約

#### domainの絶対ルール

- domainはRailsに依存しない
- domainはActiveRecordを知らない
- domainは純粋なビジネスロジックのみ

#### infrastructureルール

- infrastructure（repositories実装）はmodelsに依存する
- コマンド・クエリ責務分離（CQRS）設計とする

---

### 依存方向まとめ

#### 中核の業務領域

```
controller
↓
domains/*/domain/use_cases 
↓
domain（entities / value_objects / services / repositories（interface））
↓
infrastructure/repositories（implementation）
↓
models（ActiveRecord実装）
```

#### 補完的な業務領域

```
controller
↓
features/*/services
↓
models（ActiveRecord実装）
```

#### 一般的な業務領域

```
controller
↓
resources
↓
models（ActiveRecord実装）
```

#### 共通処理

sharedは例外的に全方向参照可能

---

## 4. Rails特有の制約

### ActiveRecordの扱い

- domainsからは直接参照せず、repository経由でのみアクセスする
- featuresやresourcesは直接参照可能
- modelは純粋なデータ表現

---

### repositoryの責務分離（CQRS）

- Query Repository（読み取り専用）
  - データ取得
  - キャッシュ利用可能
  - パフォーマンス最適化OK
  - domainロジック禁止

- Command Repository（書き込み専用）
  - 作成・更新・削除
  - トランザクション管理の対象
  - domainルールの反映

---

### コールバック禁止方針

- callbackに業務ロジックを書かない
- 副作用はユースケースに集約する

---

### Fat Model禁止

- modelにロジックを集約しない
- 複雑化した場合はドメインモデルへ移動

---

## 5. トランザクション管理

- ユースケース層が唯一のトランザクション境界
- featuresでは必要最小限にし、以下に該当したらdomainsへ移行する
  - 条件分岐が増える
  - 状態遷移（ステータス変更ロジック）が含まれる
  - 複数のドメインルールが絡む
  - ルール変更の影響範囲が広い
  - ビジネス判断がコード内に現れ始める
- modelでは管理しない

---

## 6. 例外ルール

### 許可される例

- modelのバリデーション
- scopeによるクエリ
- 軽量なフォーマット処理（表示用途）

---

### 禁止される例

- 金額計算などの業務ロジック
- 状態遷移の判断
- 複雑な条件分岐

---

## 7. 設計判断基準

実装前に必ず判断する：

### 7-1. Q1: 複雑な業務ルールがあるか？

→ domains

### 7-2. Q2: 複雑な業務ルールはなく、処理フロー中心か？

→ features

### 7-3. Q3: 単純なCRUD処理や、外部サービスなどの共通処理か？

→ resource

### 7-4. Q4: 各レイヤーで共通利用するものか？

→ shared

---

## 8. エージェントルール

- 実装前に必ず配置先を決める
- controllerから設計を始めない
- domains または features または resource から設計を始める
- 判断に迷った場合は `boundaries.md` を参照すること

---

## 9. 禁止事項

- controllersへのロジック追加
- model肥大化
- domainsでの直接のActiveRecord使用
- featuresの無制限増殖
- ルールを無視した直書き実装</code></pre>
</div>
<p>&nbsp;</p>
<p>・「docs/rules/testing.md」（テストのルール定義）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="docs/rules/testing.md"><code># テストのルール定義

## 1. 基本方針

### 責務とテストの一致

- テストの責務境界をコード構造と一致させる
- 各レイヤーの責務 ＝ テストの責務
- 「どこで何を保証するか」を明確にする
- 同一ロジックの重複検証は禁止

---

### テストピラミッド

- E2E（web / api）：少量
- Integration（domains / features / resources）：中量
- Unit（domains）：最大（最も厚くする）
- requests（コントローラーのHTTPテスト）やE2Eは「壊れていないことの確認」に留める

---

### ビジネスロジックの責務

ビジネスロジックは性質ごとに責務を分離し、適切なレイヤーに集約する。

#### domains

- ビジネスの不変ルール（ドメイン知識そのもの）を担当する
- 例：計算ロジック、状態遷移の正当性、制約条件
- システムの「正しさの核」を担う

#### features

- ユースケース単位のビジネスフローおよび軽い判断ロジックを担当する
- 例：複数ドメインをまたぐ処理順序、条件分岐による処理選択
- ドメインルールそのものは持たず、オーケストレーションとアプリケーション判断を担う
- ドメインを利用しない範囲のユースケースを担当する

#### resources

- 単純CRUDなどの標準的なリソース操作を担当する
- ビジネス判断ロジックは持たず、処理の流れの確認に留める

#### 配置判断ルール

- 判断や分岐を伴う場合は features に配置する
- 判断を伴わない単純なCRUDは resources に配置する
- 複雑なビジネスルールが含まれる場合は domains に配置する

#### controllers / models

- ビジネスロジックは持たない
- controllers は I/O のみ
- models は永続化とデータ構造の表現のみに限定する

#### 上位レイヤーの責務

- 上位レイヤーは下位レイヤーの正しさに依存する
- ビジネスルールそのものは domains または features に集約する

### ActiveRecordの位置づけ

- models は永続化モデル（ORM）でありドメインではない
- ビジネスロジックは持たない
- データ構造の表現のみに限定する

---

## 2. specのディレクトリ構成

Railsのテストフレームワークとして「RSpec」を利用するが、`/spec`のテストとしては責務ベースでディレクトリを分離し、ドメイン駆動設計およびモジュラーモノリスの境界と一致させる。

### 基本方針

- レイヤー（unit / integration / requests / e2e）で大分類する
- その中で責務（domains / features / resourcesなど）を分割する
- ドメイン単位での分離を最優先する
- テストの配置はコードの責務境界と一致させる

---

### ディレクトリ構成

```
/spec
 ├── /unit（ユニットテスト）
 |    |
 |    ├── /models
 |    |
 |    ├── /domains
 |    |    |
 |    |    └── /domain_name
 |    |         |
 |    |         ├── /domain
 |    |         |    |
 |    |         |    ├── /entities
 |    |         |    |
 |    |         |    ├── /value_objects
 |    |         |    |
 |    |         |    └── /services
 |    |         |
 |    |         └── /use_cases
 |    |
 |    └── /shared
 |
 |
 ├── /integration（インテグレーションテスト）
 |    |
 |    ├── /domains
 |    |    |
 |    |    └── /domain_name
 |    |         |
 |    |         └── /use_cases
 |    | 
 |    ├── /features
 |    |    |
 |    |    └── /[feature_name]
 |    |         |
 |    |         └── /services（サービス層） 
 |    |
 |    | 
 |    └── /resources
 |
 |
 ├── /requests（コントローラーのHTTPのテスト）
 |    |
 |    ├── /web
 |    |
 |    └── /api（将来的にAPIモードを利用する際に使う想定）
 |
 |
 └── /e2e（E2Eテスト）
      |
      ├── /web
      |
      └── /api（将来的にAPIモードを利用する際に使う想定）
```

---

### 設計意図

#### unit

- models / domains / shared を明確に分離する
- domainsについてはドメインロジックの純粋性を保証する層とする
- domains配下はDDD構造そのものを反映し、純粋なビジネスロジックのみを対象とする

#### integration

- 複数コンポーネントの結合を検証する層
- ドメイン単体などではなく、ユースケースなどの「流れ」を保証する
- featuresは業務機能単位の統合境界として扱う
- HTTPは使用しない（Request Specは禁止）
- サービス・ユースケースを直接呼び出して検証する

#### requests

- HTTP境界の検証に限定する
- Rails controllerの責務確認を目的とする
- ビジネスロジックは一切持たず、入出力の正当性のみを保証する

#### e2e

- ユーザー視点の最終保証レイヤー
- UI / APIを含めた全体フローを確認する
- カバレッジよりも主要シナリオの成立を重視し、最小限に保つ

---

### 命名ルール

- domain_name / feature_name はアプリケーションの業務ドメイン用語を使用する
- 実装クラス名ではなく「業務単位」でディレクトリを切る
- ディレクトリ構造とモジュール境界は一致させることを前提とする

### 補足ルール

- 同一責務のテストは必ず1箇所に集約する
- unit / integration / e2e 間で同じ検証を重複させない
- domains配下が最も安定した設計境界となることを前提とする

---

## 3. レイヤー別テスト定義

### domains（中核の業務領域のレイヤー）

**方針**: 最も重要なテスト層。全ビジネスルールを保証

**ルール**
- ドメインモデル（VO / Entity / Domain Service）は Unit テストで保証する
  - 原則: 純粋ロジック（DB/HTTPなし）
  - 通常は `require "rails_helper"` を使用してよい
  - 高速化したい場合は `require "spec_helper"` のみで実行してよい
- ドメインのユースケースは Integration テストで「処理の流れ」を保証する
  - `require "rails_helper"` の利用を許可する
  - 必要に応じて ActiveRecord/DB を使ってよい（I/O込みの結合検証）
- 同一ロジックの重複検証は禁止（どこで何を保証するかを明確にする）

**必須**
- 境界値テスト
- 異常系テスト
- 不変条件の検証
- 1テスト1ルール

**禁止（Unit に限定）**
- ActiveRecord使用
- FactoryBot使用

---

### features（補完的な業務領域のレイヤー）

**方針**: ユースケースの流れを検証、ドメインを使うような複雑性はない機能の動作確認

**ルール**
- Integrationテスト
- DB使用可
- サービス単位で検証

**必須**
- 1ユースケースにつき
  - 正常系1つ
  - 主要な失敗系のみ

**禁止**
- 複雑な検証
- 分岐網羅
- 過剰なモック

---

### resources（一般的な業務領域のレイヤー）

**方針**: 単純なCRUD処理などを検証

**ルール**
- APIは Request Spec（統合テスト）、Viewは System Spec（E2Eテスト） を使用する
- 1つのリソースにつき「代表的な正常系フロー」を中心に検証する
- CRUDは個別に網羅するのではなく、「一連の操作の流れの中で確認する」
- 認証・認可・決済などの横断的関心を含めて検証する
- テストはクライアント（API）またはユーザー（View）視点で記述する
- ビジネス判断を持たないことを前提とする
- 判断が発生した時点で features へ移動する

**必須**
- 登録 → 認証 → 操作 などの主要フローが成立すること
- 認証・認可が正しく機能すること（未認証・権限違いの最低限の確認）
- リソースの作成・更新・削除のうち、少なくとも1つはフロー内で通すこと
- 正常系のエンドツーエンド動作が通ること

**禁止**
- バリデーションの網羅的テストを書くこと
- 分岐ロジック（条件ごとの挙動）を細かく検証すること
- すべてのCRUDパターンをE2Eで個別にテストすること
- 失敗ケースを過剰に書くこと（詳細は下位レイヤーで担保）
- テストケースを増やしすぎること（フロー単位で最小限に抑える）

---

### models（永続化レイヤー）

**方針**: DB構造と永続化のみ保証

**ルール**
- Model Specを使用
- DB前提

**検証対象**
- validation
- association
- scope
- simple callback（最小限）

**必須**
- DBとの整合性
- クエリ結果の正しさ

**禁止**
- ビジネスロジック
- 複雑ロジック
- ドメイン知識

---

### controllers

**方針**: I/Oのみ検証

**ルール**
- Request Specを使用
- HTTPステータス
- JSON構造
- 認証・認可

**必須**
- レスポンススキーマのみ検証

**禁止**
- ビジネスロジックの結果検証
- DB状態の詳細検証

---

### views（UI）

**方針**: ユーザー体験のみ確認

**ルール**
- System Spec（E2E）を使用
- 主要フローのみ

**必須**
- 文言完全一致に依存しない
- HTML構造に依存しすぎない

---

## 4. 横断ルール

### ４.1 責務ごとのテスト配置

| 検証対象 | 配置場所 |
|----------|----------|
| 複雑なビジネスルール | domains |
| ビジネスフロー（ユースケース） | features |
| 単純なCRUD・リソース操作 | resources |
| DB構造・バリデーション | models |
| API入出力（HTTP境界） | controllers（request spec） |
| UI体験（ユーザー操作） | views（system spec） |

- 同一内容の重複テストは禁止

---

### ４.2 モック使用ルール

**許可**
- 外部API
- 時刻
- UUID
- メール送信

**禁止**
- domainオブジェクト
- ActiveRecord
- 自作クラス同士

**原則**
- モックは外部境界のみに限定

---

### ４.3 テストデータ（FactoryBot）

| レイヤー | 使用可否 |
|----------|----------|
| domains | 原則禁止（例外的に可） |
| features | 可 |
| models | 可 |
| resources | 可 |
| controllers | 可 |

**ルール**
- 最小構成で生成
- traitで意図明示
- letのネスト禁止
- domainsでは可能な限りPORO（Plain Old Ruby Object）でテストする

---

### ４.4 トランザクション

- 各テストは独立
- テスト間依存禁止
- DBは毎回ロールバック

---

### ４.5 命名ルール

- 業務用語で記述
- 条件と結果を明確にする

```ruby
describe "create order" do
  context "when stock is enough" do
    it "creates order"
  end

  context "when stock is insufficient" do
    it "raises error"
  end
end

```

---

## ５. TDD・推奨フロー

### TDDルール

- Red → Green → Refactor
- テストなし実装禁止

### 推奨フロー

- domain → use_case → controller
- domainから開始する
- controllerから始めない

## 6. アンチパターンと対策

| アンチパターン | 対策 |
|----------|----------|
| features肥大化 | domainへ移動 |
| controllerにロジック | requestを薄く |
| domainが薄い | ロジックを集約 |
| fat model | domainへ移動 |
| FactoryBot乱用 | trait整理 |

---

## 7. 追加推奨・レビュー観点

### 推奨

- カバレッジよりも重要なユースケース・分岐の網羅性を重視する
- テストは「仕様として意味があるか」を基準に設計する（カバレッジは補助指標）

### レビュー観点

- 責務とテスト対象が一致しているか
- domainに寄せるべきロジックが適切に切り出されているか
- テストが冗長・重複していないか
- ActiveRecordに過剰なビジネスロジックが混入していないか
- 重要な分岐・例外系が落ちていないか

## まとめ

- 複雑なビジネスロジックは可能な限りdomainに集約する
- modelsは永続化を主責務とし、必要最小限のロジックのみ持つ
- テストは責務ごとに分離する
- 上位は薄く、下位は厚くする
- 配置に迷った場合は「ビジネス判断があるか」で判断する</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/AGENTS.md」（Rails専用のルール定義）</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="src/AGENTS.md"><code># Rails専用のルール定義

## 概要

このディレクトリ（src）はRails実装の「実行領域」であり、
設計判断はすべて docs/rules 側に集約されている。

src内では設計判断を行わず、既存ルールを適用するのみとする。

---

## Railsの設計思想（重要）

本プロジェクトはRailsの思想である 
**「設計より規約（Convention over Configuration）」を強く採用する。**

そのため以下を前提とする：

- 明示的な設計よりも、規約（Rails標準・docs/rules）を優先する
- 個別判断やカスタム設計は原則として禁止する
- 「設計を考えること」自体をsrcでは行わない

---

## 規約と設計の関係

本プロジェクトにおける「規約」とは以下を指す：

- Railsのフレームワーク規約
- `docs/rules/*` に定義されたプロジェクト規約

これらは実質的に**設計そのものの代替物**であり、
srcはそれを機械的に適用するだけの層である。

---

## ルール参照構造

すべての実装は以下のルールに従う：

- `docs/rules/architecture.md`（設計思想）
- `docs/rules/boundaries.md`（配置判断）
- `docs/rules/rails.md`（Rails実装ルール）
- `docs/rules/database.md`（データ制約）
- `docs/rules/testing.md`（テスト方針）

---

## src配下の責務

srcは「実装のみを行う領域」であり、
以下の責務に限定される：

- MVC構造の具体的実装
- モジュラーモノリス構造のコード配置
- `docs/rules/rails.md` に従った実装

---

## 実装ルール（重要）

srcでは以下を行ってはならない：

- ルールの解釈や再定義
- 設計判断
- 技術選定の追加判断

実装は常に docs/rules を唯一の判断基準として適用すること。

---

## このディレクトリの役割

- Railsコードの実装専用領域
- MVC構造の具体的なコード表現
- モジュラーモノリス構造の物理実装

---

## 禁止事項

- src配下に設計判断を書くこと
- domains / features / resourcesの定義を再説明すること
- database設計方針を記述すること
- テスト戦略を記述すること
- docs側のルールを上書きすること

---

## エージェントの振る舞い

- 判断はすべて docs/rules に従う
- srcは「適用のみ」を行う
- 迷った場合は `docs/rules/boundaries.md` を最優先で参照する
- 「どう設計するか」は考えない
- 「どの規約に従うか」だけを選択する
- 規約が曖昧な場合のみ docs/rules 側に遷移する

---

## 補足（重要）

srcは「設計を実装する場所」ではなく 
**「規約をコードに写像する機械である」**

---

## 実装後の必須チェック（重要）

コード修正・追加が一段落した場合、エージェントは必ず以下のコマンドを順番に実行し、
警告・エラーが存在しない状態を維持すること。

### コマンド一覧

#### 1. フォーマット修正（レイアウトのみ自動修正）

`docker compose run --rm app bundle exec rubocop -A --only Layout`

---

#### 2. 静的コード解析（Rubocop）

`docker compose run --rm app bundle exec rubocop`

---

#### 3. ビューLint（ビュー変更時のみ）

※ app/views に変更があった場合のみ実行

`docker compose run --rm app bundle exec erb_lint app/views`

---

#### 4. 脆弱性チェック（Brakeman）

`docker compose run --rm app bundle exec brakeman --no-pager`

---

#### 5. テスト実行（RSpec）

`docker compose exec -T app bundle exec rspec`

---

### 実行ルール

- 上記は「実装完了後に必ず順番に実行する」
- エラー・警告がある状態で完了としてはならない
- フォーマット（Layout）のみ自動修正を許可する（rubocop -A --only Layout）
- 上記以外の rubocop の自動修正（-A）は使用してはならない
- rubocop の指摘は自動修正に依存せず、必ずコード側で修正する
- テストが失敗している状態で終了してはならない
- すべてのチェックが通過するまで修正と再実行を繰り返す

---

### 位置づけ

これらのチェックは「設計判断」ではなく、
**規約の適用結果を検証する工程**である。

したがって src の責務に含まれる。
</code></pre>
</div>
<p><span style="color: #ff0000;">※実装に関しては、Railsの設計思想「設定より規約」を重視した方がAI駆動開発との相性がいいだろうと想定しています。</span></p>
<p>&nbsp;</p>
<p>・「AGENTS.md」（全体のルール定義）</p>
<div>
<div>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text" data-file="AGENTS.md"><code># 全体のルール定義

## 概要

本プロジェクトはマルチエージェント構成（Codex Agents）を前提とする。

以下のエージェントが `.codex/agents` に定義されており、
すべての開発はこれらの役割分担に従って実行される。

---

## エージェント構成

### 利用エージェント

- pm
- 要件整理・仕様の定義
- ユーザーストーリー作成
- タスク分解および実行順序の決定
- 必要に応じた軽量な設計判断（実装詳細には踏み込まない）

- tester
- テスト設計（REDフェーズ）
- テストコード作成
- 期待仕様の定義

- implementer
- 実装（GREENフェーズ）
- `rails.md`に従ったコード記述
- テストを通すための実装のみを行う

- reviewer
- コードレビュー
- 設計逸脱の検出
- 品質・構造の検証

---

## ワークフロー

開発は必ず以下のTDDフローに従う：

- `.codex/workflows/tdd_flow.md`

---

### 補足

各エージェントは `tdd_flow.md` に定義された順序とルールに従い、
自分の責務範囲内で処理を実行すること。

---

## ルール参照構造

エージェントは実装判断を行う前に以下を参照する：

### 全体アーキテクチャ
- `docs/rules/architecture.md`

### コード配置・境界
- `docs/rules/boundaries.md`

### Rails専用ルール
- `src/AGENTS.md`
- 実装の詳細は `docs/rules/rails.md` を参照

### データベース設計
- `docs/rules/database.md`

### テスト設計
- `docs/rules/testing.md`

---

## ルール優先順位

矛盾が発生した場合は以下の優先順位で解決する：

1. `boundaries.md`（配置判断）
2. `architecture.md`（設計思想）
3. `src/AGENTS.md`（Rails構造ルール）
4. `docs/rules/rails.md`（Rails実装ルール）
5. `database.md`（永続化制約）
6. `testing.md`（テスト方針）

---

## エージェントの責務原則

### 共通ルール

- 各エージェントは自分の責務外の判断を行わない
- 設計と実装は必ず分離する
- TDDフローをスキップしない

---

### 責務分離

- pm：仕様決定・分解・進行管理（実装禁止）
- tester：テストのみ（実装禁止）
- implementer：実装のみ（設計判断禁止）
- reviewer：検証のみ（修正実装禁止）

---

## TDD制約（重要）

- REDなしで実装を開始してはならない
- GREEN未達の状態でレビューを行わない
- reviewer NGの場合は必ずimplementerに戻る
- テストは仕様の唯一の正解として扱う

---

## 禁止事項

- 本ファイルに実装ルールを書くこと
- エージェント間の責務を曖昧にすること
- TDDフローを省略すること
- reviewerが実装修正を行うこと</code></pre>
</div>
</div>
</div>
<p>&nbsp;</p>
<h3>今回のRailsのディレクトリ構成について</h3>
<p>今回のRailsのディレクトリ構成は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>モジュラーモノリスおよびドメイン駆動設計（DDD）を前提に設計</strong></span>していますが、<span style="color: #ff0000;"><strong>アプリケーション全体に対してDDDを全面的に適用することは、本質的ではないケースも多く、実装・運用コストも高くなりがち</strong></span>です。</p>
<p>また、<strong><span style="color: #ff0000;">モジュラーモノリスやDDDを強く意識した設計は、Railsが本来持つシンプルさや生産性といった利点からは一定程度離れる</span></strong>ことにもなります。</p>
<p>そこで本構成では、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Railsの持つ開発効率の良さと、DDDによるドメインの明確化という両方の利点を活かすために、両者を組み合わせたハイブリッドなアプローチを採用</strong></span>しています。</p>
<p>具体的には、システムを以下の<strong></strong><span style="background-color: #ffc6d1;"><strong>3つの業務領域に分類することを前提</strong></span>としています。</p>
<ul>
<li><strong> 中核の業務領域 </strong></li>
<li><strong> 補完的な業務領域</strong></li>
<li><strong> 一般的な業務領域</strong></li>
</ul>
<p>このうち、ビジネス上の競争優位性に直結する<strong><span style="background-color: #ffc6d1;">「中核の業務領域」に対してのみDDDを適用</span></strong>し、<strong></strong><span style="background-color: #ffc6d1;"><strong>それ以外の領域についてはRailsの標準的な構成やシンプルな設計を採用</strong></span>します。</p>
<p>これにより、重要なドメインには十分な設計投資を行いつつ、全体としては過度に複雑化しないバランスの取れた構成を目指しています。</p>
<p>&nbsp;</p>
<h3>ハーネス設計の注意点</h3>
<p>上記では<strong><span style="color: #ff0000;">様々なドキュメントを定義してハーネス設計</span></strong>を行なっていますが、<strong><span style="color: #ff0000;">それぞれのドキュメントは完璧なものではありません。</span></strong></p>
<p>もし<span style="border-bottom: 2px solid #be3144;"><strong>実務でハーネス設計をする必要がある場合は、それぞれのプロジェクトに応じて最適なハーネス設計をする</strong></span>ようにして下さい。</p>
<p>そしてその際には、<span style="color: #ff0000;"><strong>システム開発におけるあらゆる知識（アーキテクチャ、設計、言語やフレームワーク、実装方法、AIツールの使い方など）がないと良し悪しが判断できない</strong></span>のと、<strong><span style="color: #ff0000;">プロジェクトに応じてどうするべきかの決断を迫られることもあると思う</span></strong>ので、その点は注意が必要です。</p>
<p>&nbsp;</p>
<p><span style="font-size: 14pt;"><strong>ハーネスエンジニアリングは、<span style="border-bottom: 2px solid #be3144;">システム開発におけるほぼ全工程をある程度できる人じゃないと整えられない仕事（上流工程しかできない人には無理、コードは書かなくなってもコードの実装知識は必要）</span>だなというのが、自分で試してみてよくわかりました。</strong></span></p>
<p>&nbsp;</p>
<p>また、<strong></strong><span style="background-color: #ffc6d1;"><strong>これらのドキュメントは一回作って終わりではなく、適宜改善して育てていく必要性もある</strong></span>と思うので、その点も注意しましょう。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>GitHub ActionsによるCIの導入</h2>
<p>次にCI/CDを組み込むのも試しておきたいですが、今回は本番環境などは利用しないため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>CIだけ導入</strong></span>するのを試します。</p>
<p>CI（Continuous Integration：継続的インテグレーション）は、開発者が書いたコードをこまめにまとめて、自動でテストやビルドを行い、「ちゃんと動くか」をすぐ確認する仕組みです。</p>
<p>CD（Continuous Delivery Deployment：継続的デリバリー・デプロイメント）は、テスト済みのコードを本番環境にリリースできる状態にしたり、場合によっては自動で本番に反映する仕組みです。</p>
<p>また、今回はコード管理にGitHubを利用しているので、相性がいい<span style="background-color: #ffc6d1;"><strong>GitHub Actionsを利用してCIを導入</strong></span>していきます。</p>
<p>まずは以下のコマンドを実行し、設定用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p .github/workflows &amp;&amp; touch .github/workflows/ci.yml</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".github/workflows/ci.yml"><code>name: CI

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  # 変更ファイル判定（全体トリガー）
  changes:
    runs-on: ubuntu-latest

    outputs:
      app: ${{ steps.filter.outputs.app }}

    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v6

      - name: 差分チェック
        uses: dorny/paths-filter@v4
        id: filter
        with:
          filters: |
            app:
              - 'src/**/*.rb'
              - 'src/**/*.erb'
              - 'src/Gemfile*'

  # 静的コード解析・脆弱性チェック
  lint:
    needs: changes
    if: needs.changes.outputs.app == 'true'
    runs-on: ubuntu-latest

    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v6

      - name: 環境変数ファイルをリネーム
        run: cp ./.env.example ./.env

      - name: Dockerコンテナのビルド
        run: docker compose build

      # 静的コード解析
      - name: Rubocop
        run: docker compose run --rm app bundle exec rubocop

      # ビュー用の静的コード解析
      - name: ERB Lint
        run: docker compose run --rm app bundle exec erb_lint app/views

      # Railsの脆弱性チェック
      - name: Brakeman
        run: docker compose run --rm app bundle exec brakeman --no-pager

  # テスト
  test:
    needs: changes
    if: needs.changes.outputs.app == 'true'
    runs-on: ubuntu-latest

    steps:
      - name: リポジトリをチェックアウト
        uses: actions/checkout@v6

      - name: 環境変数ファイルをリネーム
        run: cp ./.env.example ./.env

      - name: Dockerコンテナのビルド
        run: docker compose build

      - name: Dockerコンテナの起動
        run: docker compose up -d

      - name: DB接続待機処理
        run: docker compose exec app bash -c "until pg_isready -h db -U ${DB_USER}; do sleep 1; done"

      - name: DB作成とマイグレーションの実行
        run: docker compose run --rm app bundle exec rails db:prepare

      - name: テストの実行（RSpec）
        run: docker compose exec -T app bundle exec rspec
</code></pre>
</div>
<p><span style="color: #ff0000;">※起動条件はプルリクエスト（PR）作成時か、mainブランチへのプッシュやマージ実行時です。加えて対象ファイルの差分変更を検知して各種処理を実行させてます。「actions/checkout@v6」や「dorny/paths-filter@v4」はnodeのバージョンが上がった際にバージョンを上げる必要がでる可能性があります。尚、将来的にテストが多くなって完了時間が長くなった場合は、テストの並列実行などの検討が必要になったりします。</span></p>
<p>&nbsp;</p>
<p>これでPR作成時やmainブランチにマージするタイミングでCIが実行されます。</p>
<p>もし<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>CIをスキップしたい場合</strong></span>は、<strong></strong><span style="background-color: #ffc6d1;"><strong>コミットメッセージの先頭に「[skip ci]」または「[ci skip]」を付ける</strong></span>とスキップ処理が可能です。</p>
<p>&nbsp;</p>
<h2>作成したプロジェクトをGitHubのリポジトリに登録</h2>
<p>次に上記で作成したプロジェクトをGitHubのリポジトリに登録します。</p>
<p>まずは以下のコマンドを実行し、コミット処理まで完了して下さい。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ git add -A
$ git commit -m "add harness"</code></pre>
</div>
<p>&nbsp;</p>
<p>コミット完了後、GitHubのリポジトリ一覧画面を開き、画面右上の「New」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-11.png" alt="" width="2728" height="1584" class="aligncenter size-full wp-image-20344" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-11.png 2728w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-11-300x174.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-11-1024x595.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-11-768x446.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-11-1536x892.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-11-2048x1189.png 2048w" sizes="auto, (max-width: 2728px) 100vw, 2728px" />
<p>&nbsp;</p>
<p>リポジトリ作成が表示されるので、リポジトリ名を入力し、リポジトリのタイプを選択後、画面右下の「Create repository」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-12.jpg" alt="" width="3024" height="1642" class="aligncenter size-full wp-image-20345" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-12.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-12-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-12-1024x556.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-12-768x417.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-12-1536x834.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-12-2048x1112.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<p>リポジトリ作成後、画面下に実行用コマンドが表示されるので、コマンドを実行してプロジェクトをリポジトリにプッシュします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-13.jpg" alt="" width="3020" height="1646" class="aligncenter size-full wp-image-20346" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-13.jpg 3020w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-13-300x164.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-13-1024x558.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-13-768x419.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-13-1536x837.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-13-2048x1116.jpg 2048w" sizes="auto, (max-width: 3020px) 100vw, 3020px" />
<p>&nbsp;</p>
<p>プッシュ完了後、プロジェクトが反映され、かつGitHub Actionsも起動します。</p>
<p>GitHub Actionsを確認するには、メニュー「Actions」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-14.jpg" alt="" width="2722" height="1584" class="aligncenter size-full wp-image-20347" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-14.jpg 2722w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-14-300x175.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-14-1024x596.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-14-768x447.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-14-1536x894.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-14-2048x1192.jpg 2048w" sizes="auto, (max-width: 2722px) 100vw, 2722px" />
<p>&nbsp;</p>
<p>Actions画面で実行したワークフロー一覧が表示されるので、対象のワークフローをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-15.png" alt="" width="2734" height="1586" class="aligncenter size-full wp-image-20348" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-15.png 2734w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-15-300x174.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-15-1024x594.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-15-768x446.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-15-1536x891.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-15-2048x1188.png 2048w" sizes="auto, (max-width: 2734px) 100vw, 2734px" />
<p>&nbsp;</p>
<p>対象のワークフローの詳細を確認でき、全て緑色のチェックが付いて正常終了すればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-16.png" alt="" width="2722" height="1572" class="aligncenter size-full wp-image-20349" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-16.png 2722w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-16-300x173.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-16-1024x591.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-16-768x444.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-16-1536x887.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-16-2048x1183.png 2048w" sizes="auto, (max-width: 2722px) 100vw, 2722px" />
<p><span style="color: #ff0000;">※今回は全て完了するのに1分17秒かかりました。</span></p>
<p>&nbsp;</p>
<h3>mainブランチなどの保護設定をしたい場合</h3>
<p>実務などでは<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>mainブランチの保護設定</strong></span>が必要になったりしますが、その場合はリポジトリのメニューから「Settings」画面を開き、画面左のメニュー「Branches」を選択後、Branch protection rules画面が開くので、「Add branch ruleset」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-17.jpg" alt="" width="2732" height="1586" class="aligncenter size-full wp-image-20352" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-17.jpg 2732w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-17-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-17-1024x594.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-17-768x446.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-17-1536x892.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-17-2048x1189.jpg 2048w" sizes="auto, (max-width: 2732px) 100vw, 2732px" />
<p>&nbsp;</p>
<p>次にRuleset Nameを入力後、Enforcement statusを「Active」に変更し、Target branchesの項目にある「Add target」から「Include by pattern」を選択します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18.jpg" alt="" width="2726" height="1590" class="aligncenter size-full wp-image-20353" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18.jpg 2726w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-300x175.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-1024x597.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-768x448.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-1536x896.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-18-2048x1195.jpg 2048w" sizes="auto, (max-width: 2726px) 100vw, 2726px" />
<p>&nbsp;</p>
<p>次にポップアップが表示されるので、「main」を入力して「Add Inclusion pattern」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19.jpg" alt="" width="2722" height="1584" class="aligncenter size-full wp-image-20354" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19.jpg 2722w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-300x175.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-1024x596.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-768x447.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-1536x894.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-19-2048x1192.jpg 2048w" sizes="auto, (max-width: 2722px) 100vw, 2722px" />
<p>&nbsp;</p>
<p>これでTarget branchesの項目に対象のブランチ名「main」が設定されます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20.jpg" alt="" width="2724" height="1576" class="aligncenter size-full wp-image-20355" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20.jpg 2724w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-1024x592.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-768x444.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-1536x889.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-20-2048x1185.jpg 2048w" sizes="auto, (max-width: 2724px) 100vw, 2724px" />
<p>&nbsp;</p>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>マージする前にプルリクエストを必須にしたい場合は、「Require a pull request before merging」のチェック</strong></span>を付け、<span style="color: #ff0000;">必要に応じてオプションを設定</span>して下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21.jpg" alt="" width="2732" height="1582" class="aligncenter size-full wp-image-20356" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21.jpg 2732w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-1024x593.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-768x445.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-1536x889.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-21-2048x1186.jpg 2048w" sizes="auto, (max-width: 2732px) 100vw, 2732px" />
<p>&nbsp;</p>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>CIをPASS（正常終了）することを必須にしたい場合は、「Require status checks to pass」のチェック</strong></span>を付け、<span style="color: #ff0000;">必要に応じてオプションを設定</span>し、<strong></strong><span style="background-color: #ffc6d1;"><strong>「Add checks」のリストからCIの対象のジョブを検索してチェック</strong></span>を付けて下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22.jpg" alt="" width="2732" height="1578" class="aligncenter size-full wp-image-20357" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22.jpg 2732w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-300x173.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-1024x591.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-768x444.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-1536x887.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-22-2048x1183.jpg 2048w" sizes="auto, (max-width: 2732px) 100vw, 2732px" />
<p>&nbsp;</p>
<p>対象のジョブを選択後、下図のように対象のジョブが表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-23.jpg" alt="" width="2724" height="1578" class="aligncenter wp-image-20358 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-23.jpg 2724w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-23-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-23-1024x593.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-23-768x445.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-23-1536x890.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-23-2048x1186.jpg 2048w" sizes="auto, (max-width: 2724px) 100vw, 2724px" />
<p>&nbsp;</p>
<p>全ての設定完了後、画面下の「Create」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-24.jpg" alt="" width="2718" height="1572" class="aligncenter size-full wp-image-20359" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-24.jpg 2718w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-24-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-24-1024x592.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-24-768x444.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-24-1536x888.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-24-2048x1184.jpg 2048w" sizes="auto, (max-width: 2718px) 100vw, 2718px" />
<p>&nbsp;</p>
<p>次に認証を求められるので、認証をして下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25.jpg" alt="" width="2716" height="1564" class="aligncenter size-full wp-image-20360" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25.jpg 2716w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-300x173.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-1024x590.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-768x442.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-1536x885.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-25-2048x1179.jpg 2048w" sizes="auto, (max-width: 2716px) 100vw, 2716px" />
<p><span style="color: #ff0000;">※私はパスキー認証にしています。</span></p>
<p>&nbsp;</p>
<p>これでブランチ保護設定が完了です。今回は一例ですが、他にも色々条件が付けられるので、必要に応じて任意のものを設定して下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-26.jpg" alt="" width="2726" height="1592" class="aligncenter size-full wp-image-20361" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-26.jpg 2726w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-26-300x175.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-26-1024x598.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-26-768x449.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-26-1536x897.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-26-2048x1196.jpg 2048w" sizes="auto, (max-width: 2726px) 100vw, 2726px" />
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>OpenAI「Codexアプリ」のプランモードで開発計画を立てる</h2>
<p>次にAIツールとして<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>OpenAIの「Codexアプリ」を使い、プランモードを用いて簡単な機能を追加する開発計画</strong></span>を立ててみます。</p>
<p>ただし、<strong><span style="color: #ff0000;">今回はAIツールとのやりとり回数を少なくしたい</span></strong>ので、事前にある程度考えておいた機能の仕様を投げる形で進めます。</p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/openai-codex-cli-app-guide" target="_blank" rel="noopener">・OpenAI Codex CLI / Codexアプリの使い方【ChatGPT時代のAI開発ツール入門】</a></p>
<p>&nbsp;</p>
<p>では今回追加してみる機能としては、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>日本の有名人の名言を「今日の一言」としてトップページの中央にいい感じに表示させる機能</strong></span>を作ります。</p>
<p>特に<strong></strong><span style="background-color: #ffc6d1;"><strong>「今日の一言」を決める部分には簡単なロジックを持たせる</strong></span>ようにして、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>名言ドメイン</strong></span>として主に以下のような仕様とします。</p>
<p>&nbsp;</p>
<p>・追加機能の仕様まとめ</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code># 追加機能の仕様

## 名言ドメインの仕様

### 1. エンティティ「Quote」

名言ドメインのエンティティ

属性：
- id：Quoteの識別子（主キー / bigint）
- quote_text：値オブジェクト「QuoteText」
  - 値オブジェクトはバリデーションの責務を持つ

---
### 2. 値オブジェクト「QuoteText」

名言の本文を表す値オブジェクト

制約：
- 使用可能文字：
- 全角ひらがな
- 全角カタカナ
- 漢字
- 長音「ー」
- 「。」（句点）
- 「、」（読点）
- 使用不可文字：
- 英数字
- スペース
- 記号（括弧・!・?など含む）
- 正規表現でバリデーションする

正規表現の例：
```
/\A[ぁ-んァ-ヶ一-龥ー。、]+\z/
```
---

### 3. ドメインサービス「QuoteSelector」

「今日の一言」を決定するドメインサービス

役割：
- 日付と名言マスタから1件のQuoteを選定する

選定ロジック：
- 対象日付の「年内日数（day_of_year）」を取得
- quotes数で割った余りを算出
- そのindexのQuoteを返す

例：
```
index = date.yday % quotes.count
```

不変条件：
- 同じ日付 → 必ず同じ結果
- quotesが同じなら結果は完全に決定論的
- うるう年は考慮しない（ydayをそのまま使用）

例外：
- quotesが0件の場合はエラーとして例外を返す


### 4. ユースケースの処理フロー

1. quote_historiesに今日の日付があるか確認
2. あればそれを返す
3. なければ：
  - QuoteSelectorで選定
  - quote_historiesに保存
  - 返却

---

## データベースの仕様
### 1. 名言マスタテーブル「quotes」

| カラム | 型 | 説明 |
| --- | --- | --- | 
| id | bigint | PK |
| text | string | 名言本文 |
| created_at | datetime | |
| updated_at | datetime | |


### 2. 名言履歴テーブル「quote_histories」

| カラム | 型 | 説明 |
| --- | --- | --- | 
| id | bigint | PK |
| date | date | 対象日（UTC日付）、ユニークキーとする |
| quote_id | bigint | 名言マスタのid |
| created_at | datetime | |
| updated_at | datetime | |

---

### 3. 名言マスタの登録値

| id | 名言 |
| --- | --- |
| 1 | 必死に生きる生涯は光を放つ |
| 2 | 我事において後悔せず |
| 3 | 天は人の上に人を造らず人の下に人を造らず |
| 4 | 成功は失敗の積み重ね |
| 5 | 小さなことの積み重ねが遠くへ行く道 |
| 6 | いい作品はいい人生から生まれる |
| 7 | 映画は自分の手でつくるもの |
| 8 | 遠回りは無駄ではない |
| 9 | 人を相手にせず天を相手にせよ |
| 10 | 人は成功するために努力する |

---

## UIの仕様

- トップページ中央に1件表示
- 表示例：`本日の一言：〇〇〇〇〇`
- レスポンシブデザインであること
- CSSにはTailwindCSSを利用する
- 全体的なデザインは落ち着きがあっていい感じにしたい</code></pre>
</div>
<p>&nbsp;</p>
<p>・UIのイメージ画像</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-28.png" alt="" width="1498" height="830" class="aligncenter size-full wp-image-20369" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-28.png 1498w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-28-300x166.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-28-1024x567.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-28-768x426.png 768w" sizes="auto, (max-width: 1498px) 100vw, 1498px" />
<p>&nbsp;</p>
<p>ではこの追加機能の仕様を使いながら、「Codexアプリ」のプランモードを試します。</p>
<p>まずはアプリ起動後、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>画面左のメニューにあるプロジェクトの右側にあるボタン「新しいプロジェクトを追加」</strong></span>をクリックし、対象のディレクトリ（今回の例ではrails-aiddです）を開きます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-30.jpg" alt="" width="2530" height="1616" class="aligncenter size-full wp-image-20372" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-30.jpg 2530w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-30-300x192.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-30-1024x654.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-30-768x491.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-30-1536x981.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-30-2048x1308.jpg 2048w" sizes="auto, (max-width: 2530px) 100vw, 2530px" />
<p>&nbsp;</p>
<p>対象のプロジェクトを開いたあと、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>チャット欄の左下にある「+」からプランモードをON</strong></span>にします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-31.jpg" alt="" width="2542" height="1610" class="aligncenter wp-image-20373 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-31.jpg 2542w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-31-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-31-1024x649.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-31-768x486.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-31-1536x973.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-31-2048x1297.jpg 2048w" sizes="auto, (max-width: 2542px) 100vw, 2542px" />
<p>&nbsp;</p>
<p>これでプランモードの準備ができたので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>以下のメッセージ時に加えて、上記に記載した追加機能の仕様を全てコピペ</strong></span>し、チャット欄の右側にある「↑」をクリックして実行します。</p>
<p>&nbsp;</p>
<p>・メッセージ</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>「今日の一言」をトップページの中央にいい感じに表示させるだけの簡単なアプリを作りたいです。
現在の仕様は以下になるので、これをまとめた上で、確認点があれば質問して下さい。</code></pre>
</div>
<p>&nbsp;</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-32.jpg" alt="" width="2534" height="1608" class="aligncenter size-full wp-image-20374" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-32.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-32-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-32-1024x650.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-32-768x487.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-32-1536x975.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-32-2048x1300.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>実行後、<strong></strong><span style="background-color: #ffc6d1;"><strong>上記で事前に設定したハーネス設定のファイルを最初に読み込んで</strong></span>から、チャット欄に入力した命令を実行してくれます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-33.jpg" alt="" width="2530" height="1608" class="aligncenter size-full wp-image-20375" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-33.jpg 2530w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-33-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-33-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-33-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-33-1536x976.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-33-2048x1302.jpg 2048w" sizes="auto, (max-width: 2530px) 100vw, 2530px" />
<p>&nbsp;</p>
<p>実行途中で質問がある場合は聞かれますが、QuoteSelectorの選定ロジックについては「yday%count」を選択しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-34.jpg" alt="" width="2534" height="1610" class="aligncenter size-full wp-image-20376" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-34.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-34-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-34-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-34-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-34-1536x976.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-34-2048x1301.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>次に今日の日付の基準については選択肢ではなく、「「今日」の基準は<span>DB</span>には<span>UTC</span>日付で保存し、トップページに表示する際は<span>JST</span>日付にして下さい。」を入力しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-35.jpg" alt="" width="2528" height="1608" class="aligncenter size-full wp-image-20377" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-35.jpg 2528w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-35-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-35-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-35-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-35-1536x977.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-35-2048x1303.jpg 2048w" sizes="auto, (max-width: 2528px) 100vw, 2528px" />
<p>&nbsp;</p>
<p>次にquotesが0件の場合については、「例外をそのまま500」を選択しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-36.jpg" alt="" width="2536" height="1606" class="aligncenter size-full wp-image-20378" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-36.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-36-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-36-1024x648.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-36-768x486.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-36-1536x973.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-36-2048x1297.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次に日付が切り替わる境界については、「JSTで切替 + UTC表記で保存」を選択しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-37.jpg" alt="" width="2538" height="1614" class="aligncenter size-full wp-image-20379" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-37.jpg 2538w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-37-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-37-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-37-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-37-1536x977.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-37-2048x1302.jpg 2048w" sizes="auto, (max-width: 2538px) 100vw, 2538px" />
<p>&nbsp;</p>
<p>全ての質問の対応が完了後、以下のようにプランが作成されました。</p>
<p>ここでは一度チャット欄の「閉じる」をクリックして閉じます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-38.jpg" alt="" width="2532" height="1614" class="aligncenter size-full wp-image-20380" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-38.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-38-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-38-1024x653.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-38-768x490.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-38-1536x979.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-38-2048x1305.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p>次にUIのイメージ画像もプランに入れて欲しかったので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>メッセージ「トップページのUIについては、添付の画像イメージのようにしてほしいです。」と共に画像ファイルを添付して実行</strong></span>します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-39.jpg" alt="" width="2534" height="1614" class="aligncenter size-full wp-image-20381" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-39.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-39-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-39-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-39-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-39-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-39-2048x1304.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>次に追加の質問として日付に付いて聞かれたので、「JSTの暦日」を選択しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-40.jpg" alt="" width="2540" height="1616" class="aligncenter size-full wp-image-20382" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-40.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-40-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-40-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-40-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-40-1536x977.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-40-2048x1303.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次に見出しのテキストについて聞かれたので、「本日の一言」を選択しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-41.jpg" alt="" width="2534" height="1616" class="aligncenter size-full wp-image-20383" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-41.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-41-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-41-1024x653.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-41-768x490.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-41-1536x980.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-41-2048x1306.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>全ての質問の対応が完了後、以下のように再度プランが作成されました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-42.jpg" alt="" width="2534" height="1612" class="aligncenter size-full wp-image-20384" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-42.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-42-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-42-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-42-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-42-1536x977.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-42-2048x1303.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<h3>スキル「plan-to-issue」を使ってプランをGitHub Issueに登録</h3>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>作成したプランを事前に作成しておいたスキル「plan-to-issue」を使ってGitHub Issueに登録する</strong></span>のを試します。</p>
<p>ただし、<strong><span style="color: #ff0000;">スキル「plan-to-issue」の中で使っているコマンドはネットワーク通信が必要</span></strong>になるので、<strong></strong><span style="background-color: #ffc6d1;"><strong>Codexアプリの設定画面から構成を開き、ネットワークアクセスを許可するを一時的に許可</strong></span>します。</p>
<p><strong><span style="color: #ff0000;">※サンドボックスのデフォルト設定ではネットワークアクセス許可がOFFです。ONにするとネットワーク通信が可能ですが、その分プロンプトインジェクションのリスクも上がるため、必要な時以外はOFFにしてAIツールを利用するようにして下さい。</span></strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43.jpg" alt="" width="2536" height="1610" class="aligncenter size-full wp-image-20385" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-1024x650.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-1536x975.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-2048x1300.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次にチャット欄に「スキル「plan-to-issue」を使って、プランをGitHub Issueに登録して下さい。」を入力して実行します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-44.jpg" alt="" width="2530" height="1600" class="aligncenter size-full wp-image-20386" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-44.jpg 2530w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-44-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-44-1024x648.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-44-768x486.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-44-1536x971.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-44-2048x1295.jpg 2048w" sizes="auto, (max-width: 2530px) 100vw, 2530px" />
<p>&nbsp;</p>
<p>実行して完了後、以下のようにIssueが登録できました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-45.jpg" alt="" width="2536" height="1608" class="aligncenter size-full wp-image-20387" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-45.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-45-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-45-1024x649.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-45-768x487.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-45-1536x974.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-45-2048x1299.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次にGitHubの方で登録されたIssueを確認しましたが、<strong><span style="color: #ff0000;">残念ながら事前に作成しておいたスキル「plan-to-issue」の作りが悪く、プラン内容が少し要約される形になってしまっていました。</span></strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-46.jpg" alt="" width="3006" height="1628" class="aligncenter size-full wp-image-20388" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-46.jpg 3006w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-46-300x162.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-46-1024x555.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-46-768x416.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-46-1536x832.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-46-2048x1109.jpg 2048w" sizes="auto, (max-width: 3006px) 100vw, 3006px" />
<p>&nbsp;</p>
<p>なので、次は<strong></strong>チャット欄に<span style="border-bottom: 2px solid #be3144;"><strong>「計画したプラン内容の全てを含める形で、スキル「plan-to-issue」を使ってプランをGitHub Issueに登録して下さい。」を入力して実行</strong></span>し、再度GitHub Issueの登録を行いました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-47.jpg" alt="" width="2534" height="1606" class="aligncenter size-full wp-image-20389" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-47.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-47-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-47-1024x649.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-47-768x487.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-47-1536x973.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-47-2048x1298.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>これで登録されたIssueの内容が以下になります。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-48.jpg" alt="" width="3024" height="1644" class="aligncenter size-full wp-image-20390" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-48.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-48-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-48-1024x557.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-48-768x418.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-48-1536x835.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-48-2048x1113.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-49.jpg" alt="" width="3024" height="1646" class="aligncenter size-full wp-image-20391" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-49.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-49-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-49-1024x557.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-49-768x418.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-49-1536x836.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-49-2048x1115.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-50.jpg" alt="" width="3024" height="1644" class="aligncenter size-full wp-image-20392" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-50.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-50-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-50-1024x557.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-50-768x418.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-50-1536x835.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-50-2048x1113.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-51.jpg" alt="" width="3024" height="1642" class="aligncenter size-full wp-image-20393" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-51.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-51-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-51-1024x556.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-51-768x417.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-51-1536x834.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-51-2048x1112.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-52.jpg" alt="" width="3024" height="1644" class="aligncenter size-full wp-image-20394" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-52.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-52-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-52-1024x557.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-52-768x418.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-52-1536x835.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-52-2048x1113.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-53.jpg" alt="" width="3024" height="1648" class="aligncenter size-full wp-image-20395" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-53.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-53-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-53-1024x558.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-53-768x419.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-53-1536x837.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-53-2048x1116.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-54.jpg" alt="" width="3024" height="1640" class="aligncenter size-full wp-image-20396" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-54.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-54-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-54-1024x555.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-54-768x417.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-54-1536x833.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-54-2048x1111.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<p>これでなんとかプランをGitHub Issueに登録できましたが、<span style="color: #ff0000;"><strong>スキル「plan-to-issue」では画像まで登録する機能はないので、UIイメージの画像ファイルはIssueを編集して以下のように追加</strong></span>しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-55.jpg" alt="" width="3024" height="1628" class="aligncenter size-full wp-image-20397" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-55.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-55-300x162.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-55-1024x551.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-55-768x413.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-55-1536x827.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-55-2048x1103.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<h3>ここまでのトークン使用量</h3>
<p>これで「Codexアプリ」のプランモードを利用し、開発計画のプランを立てて、それをGitHub Issueに登録までできましたが、<strong><span style="background-color: #ffc6d1;">トークン使用量を確認してみたところ100%→80%になっていました。</span></strong></p>
<p>今回は無料プランで試していますが、試したい方は参考にしてみて下さい。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-56.jpg" alt="" width="2532" height="1608" class="aligncenter size-full wp-image-20398" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-56.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-56-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-56-1024x650.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-56-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-56-1536x975.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-56-2048x1301.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>OpenAI「Codexアプリ」でタスク実行を試す</h2>
<p>次に上記で作ったIssueのタスクを試してみます。</p>
<p>今回は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>あえて新しいチャットを作ってからGtiHubに登録したIssueの情報を取得してタスクを実行する</strong></span>ようにします。</p>
<p>まずは画面左上の「新しいチャット」をクリックし、先ほどとは別の新しいチャットを作ります。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-60.jpg" alt="" width="2540" height="1610" class="aligncenter size-full wp-image-20402" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-60.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-60-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-60-1024x649.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-60-768x487.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-60-1536x974.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-60-2048x1298.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次に命令としてはコマンド「<code>gh issue view {対象のIssue番号} --json title,body</code>」を実行し、タスク内容を取得して下さい。」を実行し、GitHub Issueからタスクの情報を読み込みます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-61.jpg" alt="" width="2540" height="1606" class="aligncenter size-full wp-image-20403" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-61.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-61-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-61-1024x647.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-61-768x486.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-61-1536x971.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-61-2048x1295.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次に<span style="color: #ff0000;"><strong>タスク実行の際はネットワークアクセスをOFFにした方がいい</strong></span>ため、先ほどと同様に設定画面を開き、ネットワークアクセスを許可するをOFFにします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-62.jpg" alt="" width="2526" height="1606" class="aligncenter size-full wp-image-20404" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-62.jpg 2526w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-62-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-62-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-62-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-62-1536x977.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-62-2048x1302.jpg 2048w" sizes="auto, (max-width: 2526px) 100vw, 2526px" />
<p>&nbsp;</p>
<p>次に命令として「ブランチを切ってから、タスク内容を実行し、<span>Rails</span>アプリの機能を作って下さい。」を実行してみます。</p>
<p><strong><span style="color: #ff0000;">※後から確認したところ、今回の例ではマルチエージェントでタスク処理されてませんでした。マルチエージェントを使いたい場合は、命令に「マルチエージェントでやって」みたいに具体的に命令した方がいいようです。</span></strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-63.jpg" alt="" width="2526" height="1608" class="aligncenter size-full wp-image-20405" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-63.jpg 2526w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-63-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-63-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-63-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-63-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-63-2048x1304.jpg 2048w" sizes="auto, (max-width: 2526px) 100vw, 2526px" />
<p>&nbsp;</p>
<p>タスクの実行途中で承認を求められるので、適宜「はい」を選択して許可します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-64.jpg" alt="" width="2534" height="1606" class="aligncenter size-full wp-image-20406" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-64.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-64-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-64-1024x649.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-64-768x487.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-64-1536x973.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-64-2048x1298.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-65.jpg" alt="" width="2530" height="1616" class="aligncenter size-full wp-image-20407" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-65.jpg 2530w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-65-300x192.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-65-1024x654.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-65-768x491.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-65-1536x981.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-65-2048x1308.jpg 2048w" sizes="auto, (max-width: 2530px) 100vw, 2530px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-66.jpg" alt="" width="2522" height="1606" class="aligncenter size-full wp-image-20408" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-66.jpg 2522w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-66-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-66-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-66-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-66-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-66-2048x1304.jpg 2048w" sizes="auto, (max-width: 2522px) 100vw, 2522px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-67.jpg" alt="" width="2540" height="1608" class="aligncenter size-full wp-image-20409" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-67.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-67-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-67-1024x648.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-67-768x486.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-67-1536x972.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-67-2048x1297.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-68.jpg" alt="" width="2534" height="1604" class="aligncenter size-full wp-image-20410" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-68.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-68-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-68-1024x648.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-68-768x486.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-68-1536x972.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-68-2048x1296.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-69.jpg" alt="" width="2534" height="1616" class="aligncenter size-full wp-image-20411" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-69.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-69-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-69-1024x653.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-69-768x490.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-69-1536x980.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-69-2048x1306.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-70.jpg" alt="" width="2532" height="1612" class="aligncenter size-full wp-image-20412" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-70.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-70-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-70-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-70-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-70-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-70-2048x1304.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-71.jpg" alt="" width="2522" height="1606" class="aligncenter size-full wp-image-20413" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-71.jpg 2522w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-71-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-71-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-71-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-71-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-71-2048x1304.jpg 2048w" sizes="auto, (max-width: 2522px) 100vw, 2522px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-72.jpg" alt="" width="2532" height="1620" class="aligncenter size-full wp-image-20414" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-72.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-72-300x192.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-72-1024x655.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-72-768x491.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-72-1536x983.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-72-2048x1310.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-73.jpg" alt="" width="2530" height="1614" class="aligncenter size-full wp-image-20415" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-73.jpg 2530w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-73-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-73-1024x653.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-73-768x490.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-73-1536x980.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-73-2048x1307.jpg 2048w" sizes="auto, (max-width: 2530px) 100vw, 2530px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-74.jpg" alt="" width="2532" height="1618" class="aligncenter size-full wp-image-20416" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-74.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-74-300x192.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-74-1024x654.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-74-768x491.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-74-1536x982.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-74-2048x1309.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-75.jpg" alt="" width="2532" height="1608" class="aligncenter size-full wp-image-20417" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-75.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-75-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-75-1024x650.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-75-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-75-1536x975.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-75-2048x1301.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-76.jpg" alt="" width="2530" height="1606" class="aligncenter size-full wp-image-20418" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-76.jpg 2530w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-76-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-76-1024x650.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-76-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-76-1536x975.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-76-2048x1300.jpg 2048w" sizes="auto, (max-width: 2530px) 100vw, 2530px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-77.jpg" alt="" width="2526" height="1612" class="aligncenter size-full wp-image-20419" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-77.jpg 2526w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-77-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-77-1024x653.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-77-768x490.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-77-1536x980.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-77-2048x1307.jpg 2048w" sizes="auto, (max-width: 2526px) 100vw, 2526px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-78.jpg" alt="" width="2528" height="1610" class="aligncenter size-full wp-image-20420" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-78.jpg 2528w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-78-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-78-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-78-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-78-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-78-2048x1304.jpg 2048w" sizes="auto, (max-width: 2528px) 100vw, 2528px" />
<p>&nbsp;</p>
<p>15回ほど承認を求められて面倒でしたが、<strong></strong><span style="border-bottom: 2px solid #be3144; font-size: 18pt;"><strong>約42分で完了</strong></span>しました！</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-79.jpg" alt="" width="2528" height="1598" class="aligncenter size-full wp-image-20421" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-79.jpg 2528w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-79-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-79-1024x647.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-79-768x485.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-79-1536x971.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-79-2048x1295.jpg 2048w" sizes="auto, (max-width: 2528px) 100vw, 2528px" />
<p>&nbsp;</p>
<p>では動作確認するため、以下のコマンドを実行し、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>Dockerコンテナの再起動</strong></span>をします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec app rails db:prepare
$ docker compose down
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ブラウザで「http://localhost:3000」を開いて確認</strong></span>すると、以下のように<span style="font-size: 18pt;"><strong>想定通りの画面ができてました！！！</strong></span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-80.png" alt="" width="2726" height="1836" class="aligncenter size-full wp-image-20422" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-80.png 2726w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-80-300x202.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-80-1024x690.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-80-768x517.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-80-1536x1035.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-80-2048x1379.png 2048w" sizes="auto, (max-width: 2726px) 100vw, 2726px" />
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="font-size: 24pt;"><strong>す、凄すぎる！！！</strong></span></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>私の想定だと、<span style="color: #ff0000;"><strong>今回のタスクを実際に人がやろうとした場合、だいたい2営業日ぐらいはかかるかなというイメージだった</strong></span>ので、それを考えると<span style="font-size: 18pt;"><strong><span style="color: #3366ff;">約23倍の効率化を実現</span></strong></span>できてます！！</p>
<p><span style="color: #ff0000;">※960分（8時間 × 2日 × 60分） ÷ 42分 ≈ 22.86 という計算</span></p>
<p>&nbsp;</p>
<p>ちなみに今回は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>レスポンシブデザイン（異なる画面サイズに合わせて表示を自動で最適化する手法）にも対応させるようなタスクにしていた</strong></span>ので、ブラウザのツールでDimentionsに「iPhone SE」を選ぶと、以下のように<strong><span style="border-bottom: 2px solid #be3144;">画面サイズに合った表示になります！</span></strong></p>
<img loading="lazy" decoding="async" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-82.jpg" alt="" width="2726" height="1820" class="aligncenter size-full wp-image-20425" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-82.jpg 2726w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-82-300x200.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-82-1024x684.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-82-768x513.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-82-1536x1026.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-82-2048x1367.jpg 2048w" sizes="auto, (max-width: 2726px) 100vw, 2726px" />
<p>&nbsp;</p>
<h3>ここまでのトークン使用量</h3>
<p>ここまでのトークン使用量も確認してみましたが、<strong></strong><span style="background-color: #ffc6d1;"><strong>先ほどの80%→47%になっていたため、1タスクで23%ほど消費</strong></span>しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-81.jpg" alt="" width="2534" height="1606" class="aligncenter wp-image-20423 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-81.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-81-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-81-1024x649.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-81-768x487.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-81-1536x973.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-81-2048x1298.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<h3>コードも確認してみる</h3>
<p>アプリとしては想定通りのものができたようなので、実際に作成されたコードの方も確認してみましたが、<span style="color: #3366ff;"><strong>事前にしっかりハーネス設計していたため、それがちゃんと反映されて想定通りのディレクトリ構成で作成</strong></span>されてました。</p>
<p>ただ細かい部分としては、<span style="color: #ff0000;"><strong>ドメインサービスのメソッドに「!」が付いていて、それはいらないなと思った</strong></span>ので、<strong></strong><span style="background-color: #ffc6d1;"><strong>ハーネス設計として「Ruby Style Guide」のようなものが追加であった方がよさそう</strong></span>かなとは思いました。</p>
<p><span style="color: #ff0000;">※「!」については、通常は破壊的変更があるような、危険性があるメソッドに対して「!」を使い、ほとんどは通常のメソッドと対のメソッドして作ったりするため、今回の例では不要。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-83.jpg" alt="" width="2364" height="1578" class="aligncenter size-full wp-image-20426" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-83.jpg 2364w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-83-300x200.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-83-1024x684.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-83-768x513.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-83-1536x1025.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-83-2048x1367.jpg 2048w" sizes="auto, (max-width: 2364px) 100vw, 2364px" />
<p>&nbsp;</p>
<p>また、<strong><span style="color: #ff0000;">日付の部分がなんか微妙だなと思っていて、プランの方の仕様を確認したら、ちゃんと仕様を詰めれてなかった</span></strong>のでそのようになっていました。</p>
<p><span style="color: #3366ff;"><strong>ただプラン通りには作っている</strong></span>ので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>こういうバグりそうな仕様に関しては、ちゃんと設計した方がいい</strong></span>なという感じです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-84.png" alt="" width="1858" height="466" class="aligncenter size-full wp-image-20427" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-84.png 1858w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-84-300x75.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-84-1024x257.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-84-768x193.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-84-1536x385.png 1536w" sizes="auto, (max-width: 1858px) 100vw, 1858px" />
<p><span style="color: #ff0000;">※Railsの設定でタイムゾーンを「Asia/Tokyo」にしているので、今回の場合は「jst_date = jst_zone.today」だけ使えばよかった感じです。</span></p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong><span style="border-bottom: 2px solid #be3144;">プランを作る時にちゃんと仕様をチェックしましょう！</span></strong></span></p>
<p>&nbsp;</p>
<h3>マルチエージェントの起動方法について</h3>
<p>後から確認したところ、<strong><span style="color: #ff0000;">今回の例ではマルチエージェントが起動していません</span></strong>でした。</p>
<p><strong><span style="background-color: #ffc6d1;">マルチエージェントで起動したい場合は、チャットの命令として「マルチエージェントでやって」みたいに明示的な命令をした方がいいようです。</span></strong></p>
<p>例えば<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>マルチエージェントで起動</strong></span>している場合は、以下のように<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>チャット欄の上に「Background Agent」の表示が出る</strong></span>ようになっています。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-150.jpg" alt="" width="2532" height="1608" class="aligncenter size-full wp-image-20467" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-150.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-150-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-150-1024x650.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-150-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-150-1536x975.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-150-2048x1301.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p>尚、マルチエージェントのレビュワーに前回のコミットをレビューしてもらったところ、コード修正が必要な箇所があったり、逆にドキュメント側（docs/rules/testing.md）を修正した方がいい箇所が見つかりました。</p>
<p>特に<strong></strong><span style="background-color: #ffc6d1;"><strong>初期段階でドキュメント側の仕様が固まってないような場合は、レビューさせてドキュメント側の仕様を改善していくのも大事</strong></span>になりそうです。</p>
<p><strong><span style="color: #ff0000;">※モジュラーモノリスやDDDを採用するとRails標準の書き方からズレるため、その点の仕様はハーネス設計のドキュメントでしっかり定義する必要があります。</span></strong></p>
<p>&nbsp;</p>
<h2>スキル「auto-commit」でコミット処理をする</h2>
<p>次に事前に作ったスキル「auto-commit」を使ってコミット処理をさせるため、まずは先ほどと同様に設定画面から<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ネットワークアクセスを許可するを一時的に許可</strong></span>します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43.jpg" alt="" width="2536" height="1610" class="aligncenter size-full wp-image-20385" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-300x190.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-1024x650.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-1536x975.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-43-2048x1300.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次にこのままコミット処理をさせようと思いましたが、どうやら<strong><span style="color: #ff0000;">ネットワーク設定を変えるとファイル「.codex/config.toml」の方が以下のように自動で更新されてしまう</span></strong>ため、このままコミットできません。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-85.png" alt="" width="2372" height="1568" class="aligncenter size-full wp-image-20429" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-85.png 2372w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-85-300x198.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-85-1024x677.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-85-768x508.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-85-1536x1015.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-85-2048x1354.png 2048w" sizes="auto, (max-width: 2372px) 100vw, 2372px" />
<p>&nbsp;</p>
<p>そこで以下のような命令<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「.codex/config.tomlのファイルだけ更新される前に戻して、それ以外はスキル「auto-commit」を使ってコミットして下さい。」で実行</strong></span>を試してみます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-86.jpg" alt="" width="2532" height="1612" class="aligncenter size-full wp-image-20432" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-86.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-86-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-86-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-86-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-86-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-86-2048x1304.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p>ポップアップが表示されたので「続行」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-87.jpg" alt="" width="2518" height="1602" class="aligncenter size-full wp-image-20433" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-87.jpg 2518w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-87-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-87-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-87-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-87-1536x977.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-87-2048x1303.jpg 2048w" sizes="auto, (max-width: 2518px) 100vw, 2518px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-88.jpg" alt="" width="2526" height="1608" class="aligncenter size-full wp-image-20434" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-88.jpg 2526w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-88-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-88-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-88-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-88-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-88-2048x1304.jpg 2048w" sizes="auto, (max-width: 2526px) 100vw, 2526px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-89.jpg" alt="" width="2532" height="1612" class="aligncenter size-full wp-image-20435" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-89.jpg 2532w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-89-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-89-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-89-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-89-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-89-2048x1304.jpg 2048w" sizes="auto, (max-width: 2532px) 100vw, 2532px" />
<p>&nbsp;</p>
<p>次に承認を求められましたが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>コミットメッセージが英語だったので、以下のように「コミットメッセージを日本語にしてください」を実行</strong></span>しました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-90.jpg" alt="" width="2526" height="1606" class="aligncenter size-full wp-image-20436" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-90.jpg 2526w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-90-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-90-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-90-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-90-1536x977.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-90-2048x1302.jpg 2048w" sizes="auto, (max-width: 2526px) 100vw, 2526px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-91.jpg" alt="" width="2522" height="1616" class="aligncenter size-full wp-image-20437" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-91.jpg 2522w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-91-300x192.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-91-1024x656.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-91-768x492.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-91-1536x984.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-91-2048x1312.jpg 2048w" sizes="auto, (max-width: 2522px) 100vw, 2522px" />
<p>&nbsp;</p>
<p>これでコミット処理が終わりました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-92.jpg" alt="" width="2528" height="1606" class="aligncenter size-full wp-image-20438" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-92.jpg 2528w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-92-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-92-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-92-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-92-1536x976.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-92-2048x1301.jpg 2048w" sizes="auto, (max-width: 2528px) 100vw, 2528px" />
<p>&nbsp;</p>
<p>ターミナルで確認すると、ちゃんとコミット完了してました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-93.png" alt="" width="954" height="180" class="aligncenter size-full wp-image-20439" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-93.png 954w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-93-300x57.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-93-768x145.png 768w" sizes="auto, (max-width: 954px) 100vw, 954px" />
<p>&nbsp;</p>
<h2>スキル「auto-pr」で自動PR作成を試す</h2>
<p>次に事前に作ったスキル「auto-pr」を使って自動PR作成を試します。</p>
<p>チャット欄に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「スキル「auto-pr」を使ってPR作成して下さい。」を入力して実行</strong></span>します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-100.jpg" alt="" width="2528" height="1606" class="aligncenter size-full wp-image-20441" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-100.jpg 2528w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-100-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-100-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-100-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-100-1536x976.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-100-2048x1301.jpg 2048w" sizes="auto, (max-width: 2528px) 100vw, 2528px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-101.jpg" alt="" width="2524" height="1604" class="aligncenter size-full wp-image-20442" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-101.jpg 2524w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-101-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-101-1024x651.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-101-768x488.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-101-1536x976.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-101-2048x1302.jpg 2048w" sizes="auto, (max-width: 2524px) 100vw, 2524px" />
<p>&nbsp;</p>
<p>承認を求められたので「はい」を選択。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/8d8b6e74a1581bb9f04cd41a04378416.jpg" alt="" width="2534" height="1614" class="aligncenter size-full wp-image-20448" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/8d8b6e74a1581bb9f04cd41a04378416.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2026/04/8d8b6e74a1581bb9f04cd41a04378416-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/8d8b6e74a1581bb9f04cd41a04378416-1024x652.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/8d8b6e74a1581bb9f04cd41a04378416-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/8d8b6e74a1581bb9f04cd41a04378416-1536x978.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/8d8b6e74a1581bb9f04cd41a04378416-2048x1304.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<p>これで自動PR作成が終わりました。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-103.jpg" alt="" width="2536" height="1616" class="aligncenter size-full wp-image-20444" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-103.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-103-300x191.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-103-1024x653.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-103-768x489.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-103-1536x979.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-103-2048x1305.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次にGitHubのPull requestsを確認すると、想定通りにPRが作成されました。</p>
<p>今回はCIも設定していたためそれもちゃんと起動しているのと、タスク実行の際にタスク情報を取得していたから？かわかりませんが、<span style="color: #3366ff;"><strong>背景・目的に対象のIssueのリンクが付与されていたのが良かった</strong></span>です。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-104.jpg" alt="" width="3016" height="1636" class="aligncenter size-full wp-image-20445" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-104.jpg 3016w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-104-300x163.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-104-1024x555.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-104-768x417.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-104-1536x833.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-104-2048x1111.jpg 2048w" sizes="auto, (max-width: 3016px) 100vw, 3016px" />
<p>&nbsp;</p>
<p>内容を確認した際に、<span style="color: #ff0000;"><strong>「テスト確認項目」と「未対応・今後の課題」の内容がイマイチだったので、ここはスキル「auto-pr」の改善ポイント</strong></span>かなと思います。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-105.jpg" alt="" width="3024" height="1600" class="aligncenter size-full wp-image-20446" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-105.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-105-300x159.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-105-1024x542.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-105-768x406.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-105-1536x813.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-105-2048x1084.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p>&nbsp;</p>
<p>事前にCIのステータスチェックも設定していたため、一番下にはステータスチェックの内容が表示されています。</p>
<p>そんな感じで、スキル「auto-pr」による<span style="color: #3366ff;"><strong>自動PR作成もほぼ想定通りに完了</strong></span>できたので、<span style="color: #ff0000;"><strong>この後の流れとしては人間がレビューしていく感じ</strong></span>になります。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-106.jpg" alt="" width="3024" height="1632" class="aligncenter size-full wp-image-20447" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-106.jpg 3024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-106-300x162.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-106-1024x553.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-106-768x414.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-106-1536x829.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-106-2048x1105.jpg 2048w" sizes="auto, (max-width: 3024px) 100vw, 3024px" />
<p><span style="color: #ff0000;">※CIを通らないとマージ不可にする設定です。</span></p>
<p>&nbsp;</p>
<h3>レビューした方がいいこと</h3>
<p>AI駆動開発によって開発効率は劇的に上がりますが、<strong><span style="color: #ff0000;">品質担保の面ではその分レビュー工数が増えます。</span></strong></p>
<p>ただ<strong><span style="color: #ff0000;">全てをレビューするのは現実的ではない</span></strong>ため、<strong><span style="background-color: #ffc6d1;">それをカバーするのも含めて<span style="background-color: #ffc6d1;">今回はRailsの設計思想「設定より規約」にも注目</span></span><span style="background-color: #ffc6d1;"></span></strong>しています。</p>
<p>少なからず、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>以下の点についてはしっかりレビューするのが大事</strong></span>です。</p>
<div class="supplement boader">
<ul>
<li> <strong>要件を満たしているかどうか</strong></li>
<li><strong> テストケースが十分かどうか（内容、網羅性、エッジケース対応など）</strong></li>
<li><strong> 重要機能についてはパフォーマンスに問題がないか</strong></li>
</ul>
</div>
<p>&nbsp;</p>
<h3>レビュー完了後にマージ</h3>
<p>レビューが終わったらPRのコメントに「LGTM！」を記載したりします。</p>
<p><span style="color: #ff0000;">※LGTMは、「Looks Good To Me」の略で、コードレビューなどで「私的に良さそう（修正不要で承認）」を意味するフレーズです。チャットツールなどで承認の意思を素早く伝える際によく利用されます。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-110.jpg" alt="" width="2726" height="1588" class="aligncenter size-full wp-image-20453" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-110.jpg 2726w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-110-300x175.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-110-1024x597.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-110-768x447.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-110-1536x895.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-110-2048x1193.jpg 2048w" sizes="auto, (max-width: 2726px) 100vw, 2726px" />
<p>&nbsp;</p>
<p>レビュー完了後にマージしたい場合は、PRの下にある「Merge pull request」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-111.jpg" alt="" width="2728" height="1586" class="aligncenter size-full wp-image-20454" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-111.jpg 2728w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-111-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-111-1024x595.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-111-768x446.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-111-1536x893.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-111-2048x1191.jpg 2048w" sizes="auto, (max-width: 2728px) 100vw, 2728px" />
<p><span style="color: #ff0000;">※今回はサクッとマージしますが、PR毎にどのブランチにマージされるかが設定されている（一番上の部分に表示されています。変更も可能です。）ので、そこが間違えてないかは気にするようにして下さい。</span></p>
<p>&nbsp;</p>
<p>次に「Confirm merge」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-112.png" alt="" width="2728" height="1576" class="aligncenter size-full wp-image-20455" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-112.png 2728w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-112-300x173.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-112-1024x592.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-112-768x444.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-112-1536x887.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-112-2048x1183.png 2048w" sizes="auto, (max-width: 2728px) 100vw, 2728px" />
<p>&nbsp;</p>
<p>これでマージ完了です。（今回の例ではmainブランチにマージされてます。）</p>
<p>また、今回はブランチを残していますが、マージ後にPull request successfully merged and closedの右側に<span style="color: #ff0000;"><strong>ブランチ削除ボタン「Delete branch」が表示されるので、ここをクリックしてブランチ削除が可能</strong></span>です。</p>
<p>実務であれば無駄なブランチが溜まっていくので、<span style="color: #ff0000;"><strong>マージ済みのブランチは削除していく方がいい</strong></span>かなと思います。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-113.jpg" alt="" width="2718" height="1576" class="aligncenter size-full wp-image-20456" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-113.jpg 2718w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-113-300x174.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-113-1024x594.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-113-768x445.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-113-1536x891.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-113-2048x1188.jpg 2048w" sizes="auto, (max-width: 2718px) 100vw, 2718px" />
<p>&nbsp;</p>
<h3>翌日</h3>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-107.png" alt="" width="2738" height="1832" class="aligncenter size-full wp-image-20451" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-107.png 2738w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-107-300x200.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-107-1024x685.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-107-768x514.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-107-1536x1028.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-107-2048x1370.png 2048w" sizes="auto, (max-width: 2738px) 100vw, 2738px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>本番環境用のDockerコンテナについて</h2>
<p>次に<strong><span style="color: #ff0000;">本番環境にデプロイする際は専用のDockefileを作って一つのDockerコンテナを作る必要</span></strong>がありますが、<strong></strong><span style="background-color: #ffc6d1;"><strong>今回利用しているRails8には最初から本番環境用を想定したDockerfile（src/Dockerfile）が入っている</strong></span>ため、これを利用すると作れます。</p>
<p>今回は割愛しますが、<span style="color: #ff0000;"><strong>事前に各種設定ファイル（src/config/environments/production.rbやsrc/config/database.ymlなど）の必要な箇所を修正</strong></span>しておいて下さい。</p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、一度起動中のDockerコンテナを止めます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down</code></pre>
</div>
<p>&nbsp;</p>
<p>次にDBはローカル開発環境用のに接続して試すため、以下のコマンドを実行してDBコンテナだけ起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose up -d db</code></pre>
</div>
<p>&nbsp;</p>
<p>次にRails8のDockerfileを使ってローカルにコンテナを作って試してみますが、以下のコマンドを実行してDockerコンテナをビルドします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker build --no-cache -t rails-aidd-app:1.0.0 -f src/Dockerfile src</code></pre>
</div>
<p><span style="color: #ff0000;">※本番環境用を想定し、タグにはバージョン「1.0.0」を付けてます。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ビルドしたDockerコンテナを起動して試しますが、<strong><span style="color: #ff0000;">このタイミングで環境変数を渡す必要</span></strong>があり、特に<strong><span style="color: #ff0000;">環境変数「RAILS_MASTER_KEY」にはファイル「src/config/master.key」中身の値を設定する必要</span></strong>があります。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker run -d \
-p 80:80 \
-e RAILS_MASTER_KEY={src/config/master.keyの中身の値} \
-e DB_HOST=host.docker.internal \
-e DB_USER=root \
-e DB_PASSWORD=root \
rails-aidd-app:1.0.0</code></pre>
</div>
<p><span style="color: #ff0000;">※Dockerコンテナ内のポートが80になっているため、ポート設定は「80:80」にしてます。また今回は例として開発環境用のDBと接続して試すため、DB_HOSTには「host.docker.internal」を設定してます。実際に本番環境で環境変数を設定する際は各種インフラにあるシークレットサービスを使って下さい。</span></p>
<p>&nbsp;</p>
<p>コマンド実行後、Docker Desktopを確認し、以下のように二つのDockerコンテナが起動できていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-120.jpg" alt="" width="2506" height="1398" class="aligncenter size-full wp-image-20458" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-120.jpg 2506w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-120-300x167.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-120-1024x571.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-120-768x428.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-120-1536x857.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-120-2048x1142.jpg 2048w" sizes="auto, (max-width: 2506px) 100vw, 2506px" />
<p><span style="color: #ff0000;">※尚、apiコンテナの方を止めたい場合は、Docker Desktopからだと簡単に削除できます。</span></p>
<p>&nbsp;</p>
<p>次に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ブラウザで「http://localhost」にアクセス</strong></span>し、以下のように想定通りの画面が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-121.png" alt="" width="2732" height="1834" class="aligncenter size-full wp-image-20459" srcset="https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-121.png 2732w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-121-300x200.png 300w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-121-1024x687.png 1024w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-121-768x516.png 768w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-121-1536x1031.png 1536w, https://tomoyuki65.com/wp-content/uploads/2026/04/rails-aidd-121-2048x1375.png 2048w" sizes="auto, (max-width: 2732px) 100vw, 2732px" />
<p>&nbsp;</p>
<h2>AIツール利用時のセキュリティ対策について</h2>
<p>AIツールは便利ですが、<strong><span style="color: #ff0000;">セキュリティ知識が無い状態で安易に利用すると、知らないうちに意図せず機密情報を漏洩し、セキュリティ問題に発展する可能性</span></strong>があります。</p>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>最低限覚えておきたいセキュリティ知識については以下の記事にまとめた</strong></span>ので、合わせでご確認下さい。</p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<p><a href="https://golang.tomoyuki65.com/generative-ai-security-secrets-zero-trust-variables" target="_blank" rel="noopener">・生成AIで機密情報は大丈夫？AIツール開発のセキュリティ対策｜ゼロトラスト・サンドボックス・環境変数（.env）管理</a></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はRuby on RailsによるAI駆動開発の実践方法についてご紹介しました。</p>
<p>実際に試してみて<span style="font-size: 18pt;"><strong><span style="color: #3366ff;">開発効率が約23倍</span></strong><span style="color: #3366ff;"><strong>だった</strong></span></span>のに非常にびっくりしましたが、これを踏まえても、もう<span style="font-size: 14pt;"><strong><span style="border-bottom: 2px solid #be3144;">AI駆動開発は避けられません。</span></strong></span></p>
<p>そして、この<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>AI駆動開発</strong></span>にあたって、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>一番大事だなと思ったところ</strong></span>としては、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ハーネス設計の部分だった</strong></span>ので、これから<strong></strong><span style="background-color: #ffc6d1;"><strong>ハーネスエンジニアリングが流行りそう</strong></span>です。</p>
<p>そんな<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ハーネスエンジニアリング</strong></span>については、システム開発におけるほぼ全工程のある程度の知識や経験がないと整えられないため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>設計もできて、コードも書けて、AIツールも使える人ができる仕事</strong></span>であり、これから<strong></strong><span style="background-color: #ffc6d1;"><strong>シニアエンジニアの需要がますます伸びそう</strong></span>です。</p>
<p>また、<span style="color: #3366ff;"><strong>今回はRails（設計思想が「設定より規約」）を使っているので少なめのハーネス設計で済んでいます</strong></span>が、<span style="color: #ff0000;"><strong>別</strong><strong><span style="color: #ff0000;">の言語</span>やフレームワークによってはもっとしっかりハーネスを作り込む必要がでる</strong></span>ため、その点は十分注意しましょう。</p>
<p>ということで、これからRuby on RailsによるAI駆動開発をしていきたい方は、ぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>
<div class="related_article cf labelnone"><a href="https://tomoyuki65.com/substack"><figure class="eyecatch thum"><img loading="lazy" decoding="async" width="486" height="290" src="https://tomoyuki65.com/wp-content/uploads/2026/07/substack-start-3-486x290.jpg" class="attachment-home-thum size-home-thum wp-post-image" alt="" /></figure><div class="meta inbox"><p class="ttl">Substackを始めました！注目のニュースレター（メルマガ）＆SNSサービス</p><span class="date gf">2026年7月2日</span></div></a></div>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/rails-ai-tdd-ddd-modular-monolith">Ruby on Railsで始めるAI駆動開発×TDD実践｜Codexとハーネスエンジニアリングで作るDDDモジュラーモノリス</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/rails-ai-tdd-ddd-modular-monolith/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>【モンハンワイルズ】復帰勢向けクリア後にやることまとめ｜拡張前に進める装備・金策・装飾品・護石・エンドコンテンツ解説</title>
		<link>https://tomoyuki65.com/monsterhunterwilds-clear-after-return-guide</link>
					<comments>https://tomoyuki65.com/monsterhunterwilds-clear-after-return-guide#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Sat, 21 Feb 2026 16:36:04 +0000</pubDate>
				<category><![CDATA[7. モンハンワイルズ]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20201</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 モンスターハンターワイルズは発売後に賛否両論ありましたが、2026年2月18日（水）に最終アップデート配信が完了し、かつ拡張コンテンツを開発中で...</p>
The post <a href="https://tomoyuki65.com/monsterhunterwilds-clear-after-return-guide">【モンハンワイルズ】復帰勢向けクリア後にやることまとめ｜拡張前に進める装備・金策・装飾品・護石・エンドコンテンツ解説</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-15-min.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-20228" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-15-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-15-min-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>モンスターハンターワイルズ</strong></span>は発売後に賛否両論ありましたが、2026年2月18日（水）に最終アップデート配信が完了し、かつ拡張コンテンツを開発中であることも公表されました。</p>
<p>モンハンワイルズ単品としては各種コンテンツが出揃ったので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>もう一度プレイしてみようかなという復帰勢の方や、拡張コンテンツ発売前に駆け込みたい方向けに、ストーリークリア後にやることをまとめ</strong></span>ておこうと思います。</p>
<p>よければぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>
<p><script async="" src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>【モンハンワイルズ】復帰勢向けクリア後にやることまとめ｜拡張前に進める装備・金策・装飾品・護石・エンドコンテンツ解説</h2>
<p>まずモンハンワイルズは、これまでモンハンシリーズをプレイしたことがない初心者でも楽しめるように、<strong><span style="color: #3366ff;">ストーリークリアまで（チャプター6-2「萌芽の産声」まで</span></strong><strong><span style="color: #3366ff;">）の難易度は優しく</span></strong>なっており、普通に遊びながらクリアしていけます。</p>
<p>ストーリークリア直前ぐらいでHRが40前後、<strong><span style="color: #ff0000;">最後のチャプターを進めるにはHR50以上必要</span></strong>になるため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ストーリークリア時点でHRが50ぐらい</strong></span>です。</p>
<p>その後は、<strong><span style="color: #ff0000;">HR100以上にしないと受注できないクエストがある</span></strong>ため、<strong><span style="border-bottom: 2px solid #be3144;">ストーリークリア後は装備を整えながらHR100を目指して進めます。</span></strong></p>
<p>&nbsp;</p>
<h2>ストーリークリア後のHR50ぐらいからHR100までにやること</h2>
<h3>武器について</h3>
<p>武器については、武器種問わず、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>生産武器を最終強化まですれば十分</strong></span>なので、何らかの生産武器を最終強化まで強化して下さい。</p>
<p>属性に関しては<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>通常使いは麻痺や爆破などがおすすめ</strong></span>で、<strong><span style="color: #ff0000;">特定のモンスターでは火属性や龍属性を使う感じ</span></strong>になります。</p>
<p>&nbsp;</p>
<h3>HR50までに作っておいた方がいいおすすめ汎用装備</h3>
<div style="overflow: auto;">
<table style="border-collapse: collapse; width: 100%; height: 351px;">
<tbody>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="background-color: #87d5ff; text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: small;"><strong>サブ</strong><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; width: 100%; height: 27px;" colspan="2"><span style="font-size: small;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>防具（スロット）/ スキル</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">頭</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ラウンドアイα（①①）/ 属性やられ耐性Lv3</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">胴</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴアメイルβ（③①）/ 回避性能Lv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">手</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴアアームβ（②②）/ 回避性能Lv1、体術Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">腰</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴアコイルβ（③②）/ 体術Lv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">脚</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴアグリーヴβ（③①①）/ 無我の境地Lv1、ひるみ軽減Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">護石</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: small;">挑戦の護石II <span style="font-size: 10pt;">/ 挑戦者Lv2</span></span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><strong><span style="font-size: small;">装飾品</span></strong></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">挑戦珠【3】×3、抗狂珠【1】×2</span></td>
</tr>
<tr>
<td style="background-color: #87d5ff; text-align: left; width: 100%;" colspan="2"><span style="font-size: small;"><strong>発動スキル</strong></span></td>
</tr>
<tr>
<td style="width: 100%; text-align: left;" colspan="2"><span style="font-size: 10pt;">挑戦者Lv5、体術Lv3、回避性能Lv3、属性やられ耐性Lv3、無我の境地Lv3、ひるみ軽減Lv1、黒蝕一体II（黒蝕竜の力）</span></td>
</tr>
</tbody>
</table>
</div>
<p><span style="color: #ff0000;">※空きスロット：②スロ×3、①スロ×3（武器種に応じて、渾身、逆襲、スタミナ急速回復、回避距離UP、耳栓、納刀術、体術、環境適応、水場・油泥適応、緩衝などを付ける）</span></p>
<p>&nbsp;</p>
<p>ゴア装備4個で<strong><span style="color: #3366ff;">シリーズスキル「黒蝕一体II」（大型モンスターと対峙した時、狂竜症に感染し、攻撃力が上昇する。克服時、さらに攻撃力が上昇する。）</span></strong>が付き、かつ<strong><span style="color: #3366ff;">「無我の境地Lv3」（狂竜ウイルスに感染した時、かなり克服しやすくなる。克服状態の時、さらに会心率+10%）</span></strong>を組み合わせて使うのが単純に強いです。</p>
<p><span style="color: #ff0000;">※狂竜症感染時に攻撃力+10、克服時にさらに攻撃力+10、会心率+25%（+15% + 無我の境地Lv3の10%）</span></p>
<p>&nbsp;</p>
<p>また、ゴア装備を使うと火耐性と雷耐性を20以上にして属性やられを防ぐことが不可能になるため、<strong><span style="border-bottom: 2px solid #be3144;">イベントクエストで生産可能な「ラウンドアイα」を使って属性やられ耐性Lv3（すべての属性やられを無効化）を付けてます。</span></strong></p>
<p>&nbsp;</p>
<p><strong>・イベントクエスト「その閃光は私に効かない」</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-1.jpg" alt="" width="2228" height="274" class="aligncenter wp-image-20203 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-1.jpg 2228w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-1-300x37.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-1-1024x126.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-1-768x94.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-1-1536x189.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-1-2048x252.jpg 2048w" sizes="auto, (max-width: 2228px) 100vw, 2228px" />
<p><span style="color: #ff0000;">※ラウンドアイαの必要素材：花舞のカタログ×2、歴戦狩猟の証Ⅱ×2、暗器蛸の尖爪×3、ユニオン鉱石×4</span></p>
<p>&nbsp;</p>
<p><strong>・挑戦の護石の素材</strong></p>
<p>挑戦の護石I（生産）：雌火竜の上鱗×6、雌火竜の翼膜×3、怪鳥の上鱗×5、竜玉×1</p>
<p>挑戦の護石II（強化）：歴戦狩猟の証Ⅲ×5、歴戦狩猟の証Ⅱ×3、波衣竜の水玉×1、火竜の紅玉×1</p>
<p>&nbsp;</p>
<h3>防具を強化するための鎧玉を集めるイベントクエスト</h3>
<p><strong>・堅鎧玉を集めるイベントクエスト「白い暴れん坊、手に負えない」</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-4.jpg" alt="" width="2226" height="300" class="aligncenter size-full wp-image-20207" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-4.jpg 2226w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-4-300x40.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-4-1024x138.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-4-768x104.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-4-1536x207.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-4-2048x276.jpg 2048w" sizes="auto, (max-width: 2226px) 100vw, 2226px" />
<p>&nbsp;</p>
<p><strong>・重鎧玉を集めるイベントクエスト「天滄無窮」</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-5.jpg" alt="" width="2224" height="270" class="aligncenter wp-image-20208 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-5.jpg 2224w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-5-300x36.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-5-1024x124.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-5-768x93.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-5-1536x186.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-5-2048x249.jpg 2048w" sizes="auto, (max-width: 2224px) 100vw, 2224px" />
<p>&nbsp;</p>
<p><span style="color: #ff0000;">※防具は鎧玉を集めて最大強化して下さい。</span></p>
<p>&nbsp;</p>
<h3>装飾品を集めるためのイベントクエスト</h3>
<p><strong>・防具用の装飾品を集めるイベントクエスト「白き王子の豪勇魂」</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-2.jpg" alt="" width="2216" height="298" class="aligncenter size-full wp-image-20205" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-2.jpg 2216w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-2-300x40.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-2-1024x138.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-2-768x103.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-2-1536x207.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-2-2048x275.jpg 2048w" sizes="auto, (max-width: 2216px) 100vw, 2216px" />
<p>&nbsp;</p>
<p><strong>・武器用の装飾品を集めるイベントクエスト「千の光はすべて刃」</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-3.jpg" alt="" width="2224" height="268" class="aligncenter size-full wp-image-20206" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-3.jpg 2224w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-3-300x36.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-3-1024x123.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-3-768x93.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-3-1536x185.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-3-2048x247.jpg 2048w" sizes="auto, (max-width: 2224px) 100vw, 2224px" />
<p>&nbsp;</p>
<p><span style="color: #ff0000;">※装飾品集めは調査クエストの方が効率がいいですが、調査クエストを集めるのが面倒ならこれらのイベントクエストで集めましょう。</span></p>
<p>&nbsp;</p>
<h3>金策について</h3>
<p>金策については色々ありますが、<span style="color: #ff0000;"><strong>モンスターを周回して余った素材を売却するのが単純に楽</strong></span>だと思います。</p>
<p>例えばラギアクルスの素材の場合、<strong><span style="color: #ff0000;">海竜の上鱗が2200z、海竜の上皮が3300zで売却可能</span></strong>です。</p>
<p>&nbsp;</p>
<h3>HR50以上である程度装備が整ったら2種類の歴戦王を狩猟して装備集め</h3>
<p><strong>・イベントクエスト「無言の閃耀」（歴戦王レ・ダウ）</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-6.jpg" alt="" width="2224" height="268" class="aligncenter size-full wp-image-20209" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-6.jpg 2224w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-6-300x36.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-6-1024x123.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-6-768x93.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-6-1536x185.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-6-2048x247.jpg 2048w" sizes="auto, (max-width: 2224px) 100vw, 2224px" />
<p>&nbsp;</p>
<p><strong>・イベントクエスト「漣の古き都に眠れば愛し」（歴戦王ウズ・トゥナ）</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-7.jpg" alt="" width="2226" height="274" class="aligncenter size-full wp-image-20210" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-7.jpg 2226w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-7-300x37.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-7-1024x126.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-7-768x95.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-7-1536x189.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-7-2048x252.jpg 2048w" sizes="auto, (max-width: 2226px) 100vw, 2226px" />
<p>&nbsp;</p>
<p><strong>・作りたい汎用装備</strong></p>
<div style="overflow: auto;">
<table style="border-collapse: collapse; width: 100%; height: 351px;">
<tbody>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="background-color: #87d5ff; text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: small;"><strong>サブ</strong><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; width: 100%; height: 27px;" colspan="2"><span style="font-size: small;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>防具（スロット）/ スキル</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">頭</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">レダゼルトヘルムγ（③）/ 弱点特効Lv1、渾身Lv1、スタミナ急速回復Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">胴</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴアメイルβ（③①）/ 回避性能Lv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">手</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">レダゼルトアームγ（③③）/ 回避距離UPLv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">腰</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴアコイルβ（③②）/ 体術Lv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">脚</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">トゥナムルグリーヴγ（③①①）/ 納刀術Lv3、挑戦者Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">護石</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: small;">抗狂の護石Ⅲ <span style="font-size: 10pt;">/ 無我の境地Lv3</span></span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><strong><span style="font-size: small;">装飾品</span></strong></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">挑戦珠【3】×4、緩衝珠【1】×1</span></td>
</tr>
<tr>
<td style="background-color: #87d5ff; text-align: left; width: 100%;" colspan="2"><span style="font-size: small;"><strong>発動スキル</strong></span></td>
</tr>
<tr>
<td style="width: 100%; text-align: left;" colspan="2"><span style="font-size: 10pt;">挑戦者Lv5、無我の境地Lv3、納刀術Lv3、体術Lv2、回避性能Lv2、回避距離UPLv2、弱点特効Lv1、渾身Lv1、スタミナ急速回復Lv1、緩衝Lv1、黒蝕一体I（黒蝕竜の力）、根性【果敢】（ヌシの魂）</span></td>
</tr>
</tbody>
</table>
</div>
<p><span style="color: #ff0000;">※空きスロット：③スロ×2、②スロ×1、①スロ×2</span></p>
<p>&nbsp;</p>
<p>歴戦王の装備を3個付けると<span style="color: #3366ff;"><strong>グループスキル「根性【果敢】（ヌシの魂）」（致命的な大ダメージを一度だけ軽減。発動前は攻撃力が上がり防御力が下がる。発動後は防御力と耐性値が上がる。）が発動</strong></span>できます。</p>
<p>ゴア装備が2個になって火力は下がりますが、<strong><span style="color: #ff0000;">星9以上のクエストでは一撃で致命傷になる技を使ってくるモンスターが多い</span></strong>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>基本的にグループスキル「根性【果敢】（ヌシの魂）」を付けるのがおすすめ</strong></span>です。</p>
<p>また、<strong><span style="color: #ff0000;">各種耐性系の珠（耐火珠【1】、耐水珠【1】、耐雷珠【1】、耐氷珠【1】、耐龍珠【1】、耐絶珠【1】、耐裂珠【1】、耐震珠【1】）を3個ずつ集めておく</span></strong>のと、<strong><span style="border-bottom: 2px solid #be3144;">属性やられ対策としては各種耐性系の珠と合わせて食事も活用することでステータスを20以上にして防げます。</span></strong></p>
<p><span style="color: #ff0000;">※3つの食材を使った食事で属性耐性+5以上にできます。または各種食事券（ログインボーナス、配信バウンティ、タルコロなどで入手可能）を使って大集会場で食事をすれば属性耐性+10にできます。</span></p>
<p>&nbsp;</p>
<p><strong>・抗狂の護石の素材</strong></p>
<p>抗狂の護石I（生産）：狂竜結晶の欠片×5、影蜘蛛の堅鋏角×2、風鋏竜の上尾爪×2、カブレライト鉱石×4</p>
<p>抗狂の護石II（強化）：狂竜結晶×5、雪獅子の剛毛×4、雪獅子の髭×3、ノヴァクリスタル×1</p>
<p>抗狂の護石III（強化）：黒蝕竜の上鱗×6、黒蝕竜の鋭惨爪×4、黒蝕竜の尖触角×2、黒蝕竜の闇玉×1</p>
<p>&nbsp;</p>
<h3>HR100まで上げる</h3>
<p>上記のような装備集めをしつつ、HRを100まで上げて下さい。</p>
<p>&nbsp;</p>
<p><script async="" src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>HR100からやること</h2>
<h3>歴戦王ヌ・エグドラを狩猟して装備集め</h3>
<p><strong>・イベントクエスト「闇き漁火よ、呪われてあれ」（歴戦王ヌ・エグドラ）</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-8.jpg" alt="" width="2224" height="270" class="aligncenter size-full wp-image-20212" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-8.jpg 2224w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-8-300x36.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-8-1024x124.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-8-768x93.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-8-1536x186.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-8-2048x249.jpg 2048w" sizes="auto, (max-width: 2224px) 100vw, 2224px" />
<p><span style="color: #ff0000;">※火耐性20以上、環境適応Lv2（Lv1：暑さ、寒さの影響を無効化する。Lv2：地形によるダメージを無効化する。）を付けるの推薦です。</span></p>
<p>&nbsp;</p>
<p><strong>・作りたい汎用装備</strong></p>
<div style="overflow: auto;">
<table style="border-collapse: collapse; width: 100%; height: 351px;">
<tbody>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="background-color: #87d5ff; text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: small;"><strong>サブ</strong><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; width: 100%; height: 27px;" colspan="2"><span style="font-size: small;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>防具（スロット）/ スキル</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">頭</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">エグゾルスヘルムγ（①①①）/ 連撃Lv3</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">胴</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴアメイルβ（③①）/ 回避性能Lv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">手</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">エグゾルスアームγ（③①①）/ 逆恨みLv3</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">腰</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴアコイルβ（③②）/ 体術Lv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">脚</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">トゥナムルグリーヴγ（③①①）/ 納刀術Lv3、挑戦者Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">護石</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: small;">自由（渾身の護石IIIなど）</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><strong><span style="font-size: small;">装飾品</span></strong></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">挑戦珠【3】×4、抗狂珠【1】×3、緩衝珠【1】×1</span></td>
</tr>
<tr>
<td style="background-color: #87d5ff; text-align: left; width: 100%;" colspan="2"><span style="font-size: small;"><strong>発動スキル</strong></span></td>
</tr>
<tr>
<td style="width: 100%; text-align: left;" colspan="2"><span style="font-size: 10pt;">挑戦者Lv5、逆恨みLv3、連撃Lv3、無我の境地Lv3、納刀術Lv3、体術Lv2、回避性能Lv2、緩衝Lv1、黒蝕一体I（黒蝕竜の力）、根性【果敢】（ヌシの魂）</span></td>
</tr>
</tbody>
</table>
</div>
<p><span style="color: #ff0000;">※空きスロット：②スロ×1、①スロ×4</span></p>
<p>&nbsp;</p>
<p>頭と手をエグゾルスγ装備に変えるとスロットを増やせ、各種耐性系の装飾品を付けやすくできます。</p>
<p>護石は武器種に合わせて最適なものを装備して下さい。（例えば太刀なら渾身の護石III、ライトボウガンなら跳躍の護石IIIなど）</p>
<p>&nbsp;</p>
<p><strong>・渾身の護石の素材</strong></p>
<p>渾身の護石I（生産）：影蜘蛛の鋭爪×6、影蜘蛛の堅鋏角×3、毒怪鳥の頭×3、鳥竜玉×1</p>
<p>渾身の護石II（強化）：歴戦狩猟の証Ⅲ×5、歴戦狩猟の証Ⅱ×3、暗器蛸の暗玉×1、煌雷竜の雷玉×1</p>
<p>渾身の護石III（強化）：SF6チケット×3、赫猿獣の堅殻×5、黒蝕竜の闇玉×1、波衣竜の水玉×1</p>
<p><span style="color: #ff0000;">※SF6チケットは、大集会場にあるスト6コラボの闘技大会クエスト「覚悟の証明、強者の証」、「うぬは真の強者たるか」で入手可能です。（スト6コラボのサイドミッション「強さの頂き、その先へ」で解放されます。）</span></p>
<p>&nbsp;</p>
<p><strong>・体術の護石の素材</strong></p>
<p>体術の護石I（生産）：赫猿獣の鱗×2、赫炎結晶×1、赫猿獣の背甲×1、クラノダスの頭殻×1</p>
<p>体術の護石II（強化）：怪鳥の上鱗×6、怪鳥の翼×4、怪鳥の耳×3、カブレライト鉱石×5</p>
<p>体術の護石III（強化）：護兇爪竜の尖爪×4、護兇爪竜の鋭牙×3、護闢獣の鋭爪×3、獣玉×1</p>
<p>体術の護石IV（強化）：黒蝕竜の上暗翼×4、黒蝕竜の尻尾×2、護兇爪竜の宝玉×1、黒蝕竜の闇玉×1</p>
<p>体術の護石V（強化）：歴戦狩猟の証Ⅲ×5、泡狐竜の紫上毛×6、泡狐竜の上錦ヒレ×4、泡狐竜の水玉×1</p>
<p>&nbsp;</p>
<p><strong>・早気の護石の素材</strong></p>
<p>早気の護石I（生産）：暗器蛸の皮×2、暗器蛸の爪×1、獄焔蛸の皮×1、暗器蛸の髄液×1</p>
<p>早気の護石II（強化）：ゴム質の上皮×4、毒怪鳥の尻尾×2、ライトクリスタル×2、鳥竜玉×1</p>
<p>早気の護石III（強化）：黄金の硬角質×6、暗器蛸の尖爪×4、闢獣の鋭牙×3、暗器蛸の暗玉×1</p>
<p>&nbsp;</p>
<p><strong>・跳躍の護石の素材</strong></p>
<p>跳躍の護石I（生産）：沙海竜の甲殻×2、沙海竜の頭殻×1、影蜘蛛の爪×1、ドラグライト鉱石×2</p>
<p>跳躍の護石II（強化）：沙海竜の堅殻×6、刺花蜘蛛の鋭爪×4、タリオスの上鱗×5、竜玉×1</p>
<p>跳躍の護石III（強化）：火竜の堅殻×6、火竜の翼×4、赫猿獣の堅甲×3、火竜の紅玉×1</p>
<p>&nbsp;</p>
<h3>防具の限界突破と強化</h3>
<p><span style="color: #3366ff;"><strong>HR100から防具の限界突破も可能</strong></span>になるため、<strong><span style="color: #ff0000;">以降の歴戦王ジン・ダハドやゴグマジオスに挑む前にちゃんと防具も強化して下さい。</span></strong></p>
<p>&nbsp;</p>
<h3>歴戦王ジン・ダハドを狩猟して装備集め</h3>
<p><strong>・イベントクエスト「ジュデッカの心臓」（歴戦王ジン・ダハド）</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-9.jpg" alt="" width="2226" height="270" class="aligncenter size-full wp-image-20213" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-9.jpg 2226w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-9-300x36.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-9-1024x124.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-9-768x93.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-9-1536x186.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-9-2048x248.jpg 2048w" sizes="auto, (max-width: 2226px) 100vw, 2226px" />
<p><span style="color: #ff0000;">※氷耐性20以上、環境適応Lv1、拘束耐性Lv3（近接のみ）、回避性能Lv5（大集会場の「あつあつアズズ料理」を食べるとお食事回避術【大】が付き、これが回避性能Lv3相当なので、装備で回避性能Lv2を付ければ実現できます。）を付けるの推薦です。</span></p>
<p>&nbsp;</p>
<p>特に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>胴の「ダハディラメイルγ」が優秀</strong></span>なので作っておきましょう。</p>
<p>&nbsp;</p>
<h3>ゴグマジオス狩猟前の準備</h3>
<p>全ての歴戦王装備を集め終わったら、次はゴグマジオスの狩猟になりますが、<strong><span style="color: #ff0000;">ゴグマジオスでは火属性武器（第1、第2フェーズで使用）と龍属性武器（最終フェーズで使用）が必要</span></strong>になるため、まだ作ってない場合は作りましょう。</p>
<p>また、上記で記載した通り、<strong><span style="color: #ff0000;">防具も限界突破および強化</span></strong>して下さい。</p>
<p>&nbsp;</p>
<h3>ゴグマジオスを狩猟して装備集め</h3>
<p><strong>・イベントクエスト「咎は戦禍に睡臥する」</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-10.jpg" alt="" width="2226" height="274" class="aligncenter size-full wp-image-20216" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-10.jpg 2226w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-10-300x37.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-10-1024x126.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-10-768x95.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-10-1536x189.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-10-2048x252.jpg 2048w" sizes="auto, (max-width: 2226px) 100vw, 2226px" />
<p><span style="color: #ff0000;">※火属性武器と龍属性武器が必須です。防具は火耐性20以上（大集会場の食事込みで実現可能です。）、耐震Lv2、水場・油泥適応Lv1、環境適応Lv1（第2フェーズで灼熱地帯になるので周回するならあると便利。無くても高台にいるヒンヤリコロモムシで代用可能。）、ハンター生活Lv1（第2フェーズの分脈束の運搬が楽になります。）を付けるの推薦です。</span></p>
<p><span style="color: #ff0000;">※第3フェーズは小型破竜筒（スリンガー）をチャージしてから弱点に当てると高ダメージを出せ、抑制にも繋がります。尚、周回であれば小型破竜筒を上手く使ってパーツ破壊を最優先にするようにしましょう。</span></p>
<p>&nbsp;</p>
<p><strong>・巨戟アーティア武器の作成</strong></p>
<p>ゴグマジオスを狩猟できるようになると、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>巨戟アーティア武器</strong></span>を作れるようになり、<strong><span style="color: #ff0000;">これが生産武器より強い</span></strong>ので優先的に作りましょう。</p>
<p><strong><span style="color: #ff0000;">一応エンドコンテンツの一つになり、最終的には厳選したいところ</span></strong>ですが、<strong><span style="border-bottom: 2px solid #be3144;">とりあえず龍属性の武器を一つ作りましょう。</span></strong></p>
<p>まず<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>アーティア武器の生産</strong></span>については、<strong><span style="color: #ff0000;">基礎攻撃力増強のパーツを3つ使って作り、復元強化でナナイロカネを50個使って強化</span></strong>して下さい。（復元強化の際に復元ボーナスがランダムで付きますが、厳選するのは大変なのでステータスはなんでもいいです。）</p>
<p>次にゴグマジオスの狩猟で入手した<strong><span style="color: #ff0000;">巨戟変異パーツを3個（一部の武器を除き、デメリットが無い属性激化タイプを選ぶ）</span></strong>使い、アーティア武器を<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>巨戟アーティア武器に強化</strong></span>します。</p>
<p>次に<strong><span style="color: #ff0000;">ナナイロカネの場合は20個</span></strong>使い、巨戟アーティア武器をさらに<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>巨戟復元強化して復元ボーナスのステータスをアップ</strong></span>します。</p>
<p>尚、<strong><span style="color: #ff0000;">巨戟アーティア武器にはシリーズスキルとグループスキルが付き、最終的にはこれを厳選します</span></strong>が、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>主に「巨戟龍の黙示録 + ヌシの魂」または「黒蝕竜の力 + ヌシの魂」の組み合わせが一番いい</strong></span>です。</p>
<p><span style="color: #ff0000;">※素材に余裕があれば、ゴグマジオスの狩猟を早くするためにも、火属性武器も同じように作って下さい。</span></p>
<p>&nbsp;</p>
<p><strong>・作りたい汎用装備</strong></p>
<div style="overflow: auto;">
<table style="border-collapse: collapse; width: 100%; height: 351px;">
<tbody>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="background-color: #87d5ff; text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: small;"><strong>サブ</strong><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; width: 100%; height: 27px;" colspan="2"><span style="font-size: small;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>防具（スロット）/ スキル</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">頭</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ラウンドアイα（①①）/ 属性やられ耐性Lv3</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">胴</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴグメイルβ（③②）/ 巧撃Lv2、早食いLv2、水場・油泥適応Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">手</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴグアームα（②①）/ 渾身Lv2、挑戦者Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">腰</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴグコイルα（②①①）/ 連撃Lv2、フルチャージLv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">脚</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴググリーヴα（③②①）/ フルチャージLv2、挑戦者Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">護石</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: small;">挑戦の護石III <span style="font-size: 10pt;">/ 挑戦者Lv3</span></span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><strong><span style="font-size: small;">装飾品</span></strong></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">緩衝珠【1】×1</span></td>
</tr>
<tr>
<td style="background-color: #87d5ff; text-align: left; width: 100%;" colspan="2"><span style="font-size: small;"><strong>発動スキル</strong></span></td>
</tr>
<tr>
<td style="width: 100%; text-align: left;" colspan="2"><span style="font-size: 10pt;">挑戦者Lv5、フルチャージLv3、属性やられ耐性Lv3、巧撃Lv2、連撃Lv2、渾身Lv2、早食いLv2、水場・油泥適応Lv1、緩衝Lv1、宣戦呼応II（巨戟龍の黙示録）</span></td>
</tr>
</tbody>
</table>
</div>
<p><span style="color: #ff0000;">※空きスロット：③スロ×2、②スロ×4、①スロ×5</span></p>
<p>&nbsp;</p>
<p>ゴグマジオス装備を4個付けると<strong><span style="color: #3366ff;">シリーズスキル「宣戦呼応II」（Iが戦闘中の大型モンスターが怒ると属性攻撃値が上昇する。IIがIの効果が上昇し、一定時間、ダメージを防ぐ防壁を得る。）</span></strong>が付き、特に<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>防壁の効果が非常に優秀（75ダメージ分の被ダメージを無効化でき、防壁発動後から2分50秒経過すれば再発動も可能）</strong></span>です。</p>
<p>歴戦王装備を外すのでグループスキル「根性【果敢】（ヌシの魂）」がなくなりますが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>食事でシルドニンニク・特秀を使うか、大集会場の食事「とれたてシルド料理」でお食事ド根性【大】（体力が約45%以上ある時、1回のみそれを超えるダメージを受けても力尽きず同時に体力が回復する。）を付与できる</strong></span>のでこれで代用可能です。</p>
<p><span style="color: #ff0000;">※巨戟アーティア武器を厳選して「巨戟龍の黙示録 + ヌシの魂」を付けられれば、「宣戦呼応II + 根性【果敢】」を付けられるようになります。（これが最終目標です。）</span></p>
<p>&nbsp;</p>
<p><strong>・挑戦の護石III</strong><strong>の素材</strong></p>
<p>挑戦の護石III（強化）：凍峰竜の狩猟証γ×3、凍峰竜の尖爪×6、巨戟龍の大爪×6、戦火の龍玉×1</p>
<p>&nbsp;</p>
<h3>星10の歴戦王アルシュベルドを狩猟して装備集め</h3>
<p><strong>・イベントクエスト「孤独から出でし君という自由」</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11.jpg" alt="" width="2222" height="322" class="aligncenter size-full wp-image-20218" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11.jpg 2222w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-300x43.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-1024x148.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-768x111.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-1536x223.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-2048x297.jpg 2048w" sizes="auto, (max-width: 2222px) 100vw, 2222px" />
<p><span style="color: #ff0000;">※いずれかの属性の巨戟アーティア武器が必須（属性武器じゃないと強化解除して大ダウン取れないため）です。防具は龍耐性20以上か属性やられ耐性Lv3（属性ダメージが大事なので）、気絶耐性Lv3（主に近接のみ）、回避性能Lv5（ガード可能武器以外の場合は食事込みで実現する。空中から突進してコンボしてくるのは最初に相手方向へ回避するといい。）を付けるの推薦です。</span></p>
<p>&nbsp;</p>
<p><strong>・作りたい汎用装備</strong></p>
<div style="overflow: auto;">
<table style="border-collapse: collapse; width: 100%; height: 351px;">
<tbody>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt; color: #ff0000;">巨戟アーティア武器（巨戟龍の黙示録 + ヌシの魂）</span></td>
</tr>
<tr style="height: 27px;">
<td style="background-color: #87d5ff; text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: small;"><strong>サブ</strong><span style="font-size: 10pt;"><strong>武器（スロット）/ レア度 / 備考</strong></span></span></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; width: 100%; height: 27px;" colspan="2"><span style="font-size: 10pt; color: #ff0000;">巨戟アーティア武器（巨戟龍の黙示録 + ヌシの魂）</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><span style="font-size: 10pt;"><strong>防具（スロット）/ スキル</strong></span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">頭</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴグヘルムα（③①）/ フルチャージLv2、属性吸収Lv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">胴</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">シュバルカメイルγ（③②）/ 属性変換Lv3、属性やられ耐性Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">手</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴグアームα（②①）/ 渾身Lv2、挑戦者Lv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">腰</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">ゴグコイルα（②①①）/ 連撃Lv2、フルチャージLv1</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">脚</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: 10pt;">シュバルカグリーヴγ（③③）/ 鎖刃刺撃Lv2、属性やられ耐性Lv2</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 8.923885%; text-align: left; height: 27px; background-color: #ededed;"><strong><span style="font-size: 10pt;">護石</span></strong></td>
<td style="width: 91.076115%; text-align: left; height: 27px;"><span style="font-size: small;">自由</span></td>
</tr>
<tr style="height: 27px;">
<td style="width: 100%; background-color: #87d5ff; height: 27px; text-align: left;" colspan="2"><strong><span style="font-size: small;">装飾品</span></strong></td>
</tr>
<tr style="height: 27px;">
<td style="text-align: left; height: 27px; width: 100%;" colspan="2"><span style="font-size: 10pt;">挑戦珠【3】×4、緩衝珠【1】×1</span></td>
</tr>
<tr>
<td style="background-color: #87d5ff; text-align: left; width: 100%;" colspan="2"><span style="font-size: small;"><strong>発動スキル</strong></span></td>
</tr>
<tr>
<td style="width: 100%; text-align: left;" colspan="2"><span style="font-size: 10pt;">挑戦者Lv5、フルチャージLv3、属性変換Lv3、属性やられ耐性Lv3、連撃Lv2、鎖刃刺撃Lv2、渾身Lv2、属性吸収Lv2、緩衝Lv1、宣戦呼応II（巨戟龍の黙示録）、加速再生I（鎖刃竜の飢餓）、根性【果敢】（ヌシの魂）</span></td>
</tr>
</tbody>
</table>
</div>
<p><span style="color: #ff0000;">※空きスロット：②スロ×3、①スロ×3</span></p>
<p>&nbsp;</p>
<p><strong><span style="color: #ff0000;">巨戟アーティア武器を厳選して「巨戟龍の黙示録 + ヌシの魂」を付与できていれ</span></strong>ば、上記の装備で<span style="color: #3366ff;"><strong>スキル「宣戦呼応II + 加速再生I + 根性【果敢】+ 属性やられ耐性Lv3」</strong></span>を同時に付けられます。</p>
<p>&nbsp;</p>
<h3>弱いモンスター用にはシリーズスキル「黒蝕一体」装備がおすすめ</h3>
<p>星10クエストなど強力なモンスター相手にはゴグマジオス装備でシリーズスキル「宣戦呼応II」を付けるのが安定ですが、それが必要ない<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>弱いモンスター用としてはシリーズスキル「黒蝕一体」を付けた装備がおすすめ</strong></span>です。</p>
<p><span style="color: #ff0000;"><strong>スキルを厳選した巨戟アーティア武器（黒蝕竜の力 + ヌシの魂）に加え、ゴア装備やナディアさんの追加サイドミッションをクリアして作れる「クイーンα」装備を組み合わせる</strong></span>と、シリーズスキル「黒蝕一体II」を組み込んで火力アップを図れます。</p>
<p>&nbsp;</p>
<h2>エンドコンテンツ</h2>
<h3>巨戟アーティア武器の厳選</h3>
<p>エンドコンテンツの一つ目は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>巨戟アーティア武器の厳選</strong></span>になります。</p>
<p>まずはアーティア武器を生産する段階において、使用している武器種の各属性ごとに<strong><span style="color: #ff0000;">復元強化で付くボーナス種類の厳選</span></strong>をします。</p>
<p>次に巨戟アーティア武器へ強化する際や、スキルの再付与にて、<strong><span style="color: #ff0000;">「巨戟龍の黙示録 + ヌシの魂」または「黒蝕竜の力 + ヌシの魂」のスキルを付けるように厳選</span></strong>します。</p>
<p>最後に巨戟復元強化で<strong><span style="color: #ff0000;">復元ボーナスのステータスアップの厳選</span></strong>をします。</p>
<p>尚、<strong><span style="color: #ff0000;">巨戟アーティア武器の厳選をやろうとすると大量のお金も必要</span></strong>になりますが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ゴグマジオスの不要な素材を売却する</strong></span>と、厳選用の資金を調達できます。</p>
<p>&nbsp;</p>
<h3>護石ガチャ</h3>
<p>エンドコンテンツの二つ目は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>星10の歴戦王クエストで入手できる「古びたお守り」によるレア度の高い護石ガチャ（レア7 or レア8の護石）</strong></span>です。</p>
<p><span style="color: #3366ff;"><strong>武器種に応じて以下の3種類のいずれかが狩猟しやすい（星9の歴戦王とそんなにかわらない）</strong></span>ので、お好みのクエストを周回して下さい。</p>
<p>&nbsp;</p>
<p><strong>・イベントクエスト「楼蘭の睥睨者」（歴戦王レ・ダウ）</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-12.jpg" alt="" width="2224" height="318" class="aligncenter wp-image-20219 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-12.jpg 2224w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-12-300x43.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-12-1024x146.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-12-768x110.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-12-1536x220.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-12-2048x293.jpg 2048w" sizes="auto, (max-width: 2224px) 100vw, 2224px" />
<p>&nbsp;</p>
<p><strong>・イベントクエスト「トゥオネラの番人より告ぐ」（歴戦王ウズ・トゥナ）</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-13.jpg" alt="" width="2220" height="358" class="aligncenter size-full wp-image-20220" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-13.jpg 2220w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-13-300x48.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-13-1024x165.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-13-768x124.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-13-1536x248.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-13-2048x330.jpg 2048w" sizes="auto, (max-width: 2220px) 100vw, 2220px" />
<p>&nbsp;</p>
<p><strong>・イベントクエスト「黒き揺り篭の吾子」（歴戦王ヌ・エグドラ）</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-14.jpg" alt="" width="2220" height="352" class="aligncenter size-full wp-image-20221" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-14.jpg 2220w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-14-300x48.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-14-1024x162.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-14-768x122.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-14-1536x244.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-14-2048x325.jpg 2048w" sizes="auto, (max-width: 2220px) 100vw, 2220px" />
<p>&nbsp;</p>
<p>または、<span style="color: #ff0000;"><strong>失敗率も高い</strong></span>ですが<strong><span style="color: #3366ff;">狩猟時の報酬が美味しいのは歴戦王アルシュベルド</span></strong>になります。</p>
<p><strong>・イベントクエスト「孤独から出でし君という自由」（歴戦王アルシュベルド）</strong></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11.jpg" alt="" width="2222" height="322" class="aligncenter size-full wp-image-20218" srcset="https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11.jpg 2222w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-300x43.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-1024x148.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-768x111.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-1536x223.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2026/02/mhwilds2602-11-2048x297.jpg 2048w" sizes="auto, (max-width: 2222px) 100vw, 2222px" />
<p>&nbsp;</p>
<p><script async="" src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回は復帰勢や拡張前に駆け込みたい人向けにストーリークリア後にやることをまとめました。</p>
<p><strong><span style="color: #ff0000;">使用している武器種によって細かい必要な装備などは変わるので、その点は別途調べていただく必要があります</span></strong>が、汎用的な部分はまとめられたと思います。</p>
<p>これからモンハンワイルズをプレイする方は、ぜひ参考にしてみて下さい！</p>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/monsterhunterwilds-clear-after-return-guide">【モンハンワイルズ】復帰勢向けクリア後にやることまとめ｜拡張前に進める装備・金策・装飾品・護石・エンドコンテンツ解説</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/monsterhunterwilds-clear-after-return-guide/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PythonのDjangoで管理画面を開発する方法まとめ</title>
		<link>https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python</link>
					<comments>https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Wed, 24 Dec 2025 09:51:14 +0000</pubDate>
				<category><![CDATA[4. プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20145</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 Webアプリケーション開発の際には、別途DB操作に関わる管理画面を作ったりしますが、PythonのDjangoを使うと簡単に管理画面が作れるらし...</p>
The post <a href="https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python">PythonのDjangoで管理画面を開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-35-min.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-20194" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-35-min.png 672w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-35-min-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>Webアプリケーション開発の際には、別途DB操作に関わる<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>管理画面</strong></span>を作ったりしますが、<strong><span style="color: #ff0000;">PythonのDjangoを使うと簡単に管理画面が作れるらしい</span></strong>というのを知ったので、試してみることにしました！</p>
<p>この記事では、そんなPythonのDjangoで管理画面を開発する方法についてまとめます。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>PythonのDjangoで管理画面を開発する方法まとめ</h2>
<p><span>まずは以下のコマンドを実行し、各種ファイルを作成します。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir django-admin &amp;&amp; cd django-admin
$ mkdir -p deploy/docker/local/py &amp;&amp; touch deploy/docker/local/py/Dockerfile
$ mkdir -p deploy/docker/local/db &amp;&amp; touch deploy/docker/local/db/Dockerfile
$ mkdir -p deploy/docker/local/db/init &amp;&amp; touch deploy/docker/local/db/init/init.sql
$ touch .env compose.yml</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル開発環境の構築については、いつものようにDockerを利用するため、試したい方は事前にDocker DesktopなどをインストールしてDockerを使える環境を準備して下さい。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「deploy/docker/local/py/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/py/Dockerfile"><code>FROM python:3.14.0-slim-trixie

# タイムゾーン設定
ENV TZ=Asia/Tokyo

# パッケージ管理用のpoetryをインストール
RUN pip3 install --no-cache-dir poetry

# [開発用] 仮想環境を作成
RUN poetry config virtualenvs.in-project true

WORKDIR /py

EXPOSE 8000</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はPythonのバージョン「3.14」を使います。各種ライブラリのパッケージ管理には「poetry」を使います。開発時専用のライブラリを入れるためにpoetryのコンフィグ設定で仮想環境設定「virtualenvs.in-project」を有効化しています。</span></p>
<p>&nbsp;</p>
<p>・「deploy/docker/local/db/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/db/Dockerfile"><code>FROM postgres:18.1

ENV LANG ja_JP.utf8

# PostgreSQLの日本語化で「ja_JP.utf8」を使うために必要
RUN apt-get update &amp;&amp; \
    apt-get install -y locales &amp;&amp; \
    rm -rf /var/lib/apt/lists/* &amp;&amp; \
    localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8</code></pre>
</div>
<p>&nbsp;</p>
<p>・「deploy/docker/local/db/init/init.sql」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-sql" data-lang="SQL" data-file="deploy/docker/local/db/init/init.sql"><code>-- usersテーブルの作成
CREATE TABLE "public"."users" ("id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "uid" character varying NOT NULL, "last_name" character varying NOT NULL, "first_name" character varying NOT NULL, "email" character varying NOT NULL, "created_at" timestamptz NOT NULL, "updated_at" timestamptz NOT NULL, "deleted_at" timestamptz NULL, PRIMARY KEY ("id"));
CREATE INDEX "user_deleted_at" ON "public"."users" ("deleted_at");
CREATE UNIQUE INDEX "users_email_key" ON "public"."users" ("email");
CREATE UNIQUE INDEX "users_uid_key" ON "public"."users" ("uid");</code></pre>
</div>
<p><span style="color: #ff0000;">※管理画面は後から作成されることが多く、対象のDBには既にテーブルが存在することを前提に試すため、DBコンテナ起動時にSQLを実行して「users」テーブルを作成しておきます。尚、今回はDBにPostgreSQLを使用します。</span></p>
<p>&nbsp;</p>
<p>・「.env」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".env"><code>ENV=local
DB_NAME=pg-db
DB_USER=pg-user
DB_PASSWORD=pg-password
DB_HOST=db-django</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル環境用の環境変数ファイル</span></p>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  django:
    container_name: django-admin
    build:
      context: .
      dockerfile: ./deploy/docker/local/py/Dockerfile
    volumes:
      - .:/py
    ports:
      - "8000:8000"
    env_file:
      - .env
    tty: true
    stdin_open: true</code></pre>
</div>
<p>&nbsp;</p>
<p><span>次に以下のコマンドを実行し、Dockerコンテナをビルドします。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p><span>次に以下のコマンドを実行し、パッケージ管理用のpoetryの初期化をします。</span></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>docker compose run --rm django poetry init</code></pre>
</div>
<p>&nbsp;</p>
<p><span>コマンド実行後、対話形式で各種設定について聞かれるので、「Package name [py]:」は「django-admin」、「Author [None, n to skip]:」は「n」、「Would you like to define your main dependencies interactively? (yes/no) [yes]」は「no」、「Would you like to define your development dependencies interactively? (yes/no) [yes]」は「no」を入力して実行し、それ以外はそのまま実行して進めます。</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1.jpg" alt="" width="1392" height="1012" class="aligncenter wp-image-20148 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1.jpg 1392w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1-300x218.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1-1024x744.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-1-768x558.jpg 768w" sizes="auto, (max-width: 1392px) 100vw, 1392px" />
<p>&nbsp;</p>
<p>完了後、poetryの設定ファイル「pyproject.toml」が作成されます。</p>
<p>次に以下のコマンドを実行し、今回利用する各種パッケージをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm django poetry add django "psycopg[binary]"
$ docker compose run --rm django poetry add --dev ruff</code></pre>
</div>
<p><span style="color: #ff0000;">※djangoの他、PostgreSQL用のドライバー「psycopg[binary]」および、開発専用ライブラリとして「ruff」（フォーマッター + 静的コード解析）を使います。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Djangoのプロジェクトを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm django poetry run django-admin startproject myapp ./src</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、以下のように各種ファイルが作成されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2.png" alt="" width="1548" height="834" class="aligncenter size-full wp-image-20151" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2.png 1548w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2-300x162.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2-1024x552.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2-768x414.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-2-1536x828.png 1536w" sizes="auto, (max-width: 1548px) 100vw, 1548px" />
<p>&nbsp;</p>
<p>次に設定用のファイル「src/myapp/settings.py」を以下のように修正します。</p>
<p>・「src/myapp/settings.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/settings.py"><code>"""
Django settings for myapp project.

Generated by 'django-admin startproject' using Django 6.0.

For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""

import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-jr21o1cjct+97fvqon)d$hdjm8oo7h0ko_th=dlv!8@ts#4!&amp;5'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # プロジェクトのディレクトリ追加
    'myapp'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'myapp.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'myapp.wsgi.application'


# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases

DATABASES = {
    # PostgreSQLへの接続設定
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ['DB_NAME'],
        'USER': os.environ['DB_USER'],
        'PASSWORD': os.environ['DB_PASSWORD'],
        'HOST': os.environ['DB_HOST'],
        'PORT': '5432',
    }
}


# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/

# 日本語化設定
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/

STATIC_URL = 'static/'</code></pre>
</div>
<p><span style="color: #ff0000;">※INSTALLED_APPSにプロジェクトのディレクトリを追加、DATABASESにPostgreSQLへの接続設定を追加、LANGUAGE_CODEとTIME_ZONEで日本語化設定をしています。</span></p>
<p>&nbsp;</p>
<p>次にファイル「deploy/docker/local/py/Dockerfile」、「compose.yml」をそれぞれ以下のように修正します。</p>
<p>・「deploy/docker/local/py/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/py/Dockerfile"><code>FROM python:3.14.0-slim-trixie

# タイムゾーン設定
ENV TZ=Asia/Tokyo

# パッケージ管理用のpoetryをインストール
RUN pip3 install --no-cache-dir poetry

# [開発用] 仮想環境を作成
RUN poetry config virtualenvs.in-project true

WORKDIR /py

# poetry.lockから依存関係をインストール
COPY pyproject.toml poetry.lock .
RUN poetry install --no-root

COPY ./src ./src

EXPOSE 8000</code></pre>
</div>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  django:
    container_name: django-admin
    build:
      context: .
      dockerfile: ./deploy/docker/local/py/Dockerfile
    command: poetry run python ./src/manage.py runserver 0.0.0.0:8000
    volumes:
      - ./pyproject.toml:/py/pyproject.toml
      - ./poetry.lock:/py/poetry.lock
      - ./src:/py/src
    ports:
      - "8000:8000"
    env_file:
      - .env
    tty: true
    stdin_open: true
    depends_on:
      - db-django
  db-django:
    container_name: django-db
    build:
      context: .
      dockerfile: ./deploy/docker/local/db/Dockerfile
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      TZ: Asia/Tokyo
      # ローカル環境でもパスワードを有効化する設定
      POSTGRES_INITDB_ARGS: --auth-local=scram-sha-256 --auth-host=scram-sha-256
    volumes:
      - ./deploy/docker/local/db/init:/docker-entrypoint-initdb.d
      - db-django-data:/var/lib/postgresql
    ports:
      - "5432:5432"
    env_file:
      - .env
volumes:
  db-django-data:</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナの再ビルドおよび起動をします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:8000」を開き、以下のような画面が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3.jpg" alt="" width="2688" height="1840" class="aligncenter wp-image-20152 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3.jpg 2688w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-300x205.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-1024x701.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-768x526.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-1536x1051.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-3-2048x1402.jpg 2048w" sizes="auto, (max-width: 2688px) 100vw, 2688px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、DBからスキーマ情報を取得します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run python ./src/manage.py inspectdb &gt; ./src/myapp/models.py</code></pre>
</div>
<p><span style="color: #ff0000;">※尚、後からテーブルを追加するような場合は、別のファイル名で出力し、元の「src/myapp/models.py」に別途手動で設定を追加するようにして下さい。</span></p>
<p>&nbsp;</p>
<p>コマンド実行後、以下のようにファイルが作成されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4.jpg" alt="" width="2372" height="1570" class="aligncenter wp-image-20153 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4.jpg 2372w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-300x200.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-1024x678.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-768x508.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-1536x1017.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-4-2048x1356.jpg 2048w" sizes="auto, (max-width: 2372px) 100vw, 2372px" />
<p><span style="color: #ff0000;">※DBからスキーマ情報を取得した場合、メタ情報の設定に「managed = False」が追加され、マイグレーション関連の処理が実行されないようになっています。</span></p>
<p>&nbsp;</p>
<p>次にファイル「src/myapp/models.py」を以下のように修正します。</p>
<p>・「src/myapp/models.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/models.py"><code>from django.db import models
from django.utils.translation import gettext_noop

class Users(models.Model):
    id = models.BigAutoField(primary_key=True)
    uid = models.CharField(unique=True)
    last_name = models.CharField()
    first_name = models.CharField()
    email = models.CharField(unique=True)
    # 作成日と更新日を自動設定するように修正
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    deleted_at = models.DateTimeField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'users'
        # 表示設定（日本語化で翻訳されないようにgettext_noopを使用）
        verbose_name = gettext_noop("User")
        verbose_name_plural = gettext_noop("Users")</code></pre>
</div>
<p><span style="color: #ff0000;">※DBスキーマ用のモデル設定はこのファイルで行います。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、管理画面用の設定ファイルを追加します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ touch src/myapp/admin.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「src/myapp/admin.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/admin.py"><code>from django.contrib import admin
from django.contrib.admin.models import LogEntry
from .models import Users

# サイト名の設定
admin.site.site_header = "Django Admin"
admin.site.site_title = "Django Admin"

# 操作履歴用にデフォルトであるモデルを管理画面に追加
admin.site.register(LogEntry)

# DBスキーマに関するモデルを管理画面に追加
admin.site.register(Users)</code></pre>
</div>
<p><span style="color: #ff0000;">※管理画面用の設定はこのファイルで行います。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、管理画面用の設定をDBに追加するためのマイグレーションを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run python ./src/manage.py migrate</code></pre>
</div>
<p><span style="color: #ff0000;">※マイグレーションを実行すると、管理画面用の各種テーブル等がDBに作成されます。もしモデルの定義のメタ情報で「managed = False」が設定されていなければ、自動でマイグレーションが実行されてテーブルなど作成されたりするので注意して下さい。尚、メタ情報で「managed = False」を設定していても、管理者ユーザーの権限設定で使用できるデータを追加するためにマイグレーションの実行は必要なので、後からモデルを追加した際は再度マイグレーションの実行が必要です。</span></p>
<p>&nbsp;</p>
<p>コマンド実行後、以下のようにマイグレーションが成功しているログ出力が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5.jpg" alt="" width="1318" height="622" class="aligncenter wp-image-20154 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5.jpg 1318w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5-300x142.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5-1024x483.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-5-768x362.jpg 768w" sizes="auto, (max-width: 1318px) 100vw, 1318px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、管理画面用のスーパーユーザーを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run python ./src/manage.py createsuperuser</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、各種入力を求められるため、今回はユーザー名「local-db-root」、メールアドレスは無し、パスワード「pass-pg-2512」を設定して実行します。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6.png" alt="" width="1482" height="176" class="aligncenter wp-image-20155 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6.png 1482w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6-300x36.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6-1024x122.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-6-768x91.png 768w" sizes="auto, (max-width: 1482px) 100vw, 1482px" />
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、コンテナを再起動させます。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:8000/admin」を開き、以下のように管理画面用のログイン画面が表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7.png" alt="" width="2678" height="1594" class="aligncenter size-full wp-image-20156" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-300x179.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-1024x610.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-768x457.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-1536x914.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-2048x1219.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-7-486x290.png 486w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>次に先ほど作成したスーパーユーザーのユーザー名「local-db-root」とパスワード「pass-pg-2512」でログインし、以下のように表示されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9.png" alt="" width="2676" height="1828" class="aligncenter size-full wp-image-20158" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-300x205.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-1024x700.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-768x525.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-1536x1049.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-9-2048x1399.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>これで<strong><span style="color: #3366ff;">DBスキーマから設定した対象テーブルのCRUD処理や、管理画面として最低限必要になる管理者ユーザー機能（パーミッション機能を含む）や、操作履歴のログ機能が簡単に実装</span></strong>できます。</p>
<p>&nbsp;</p>
<h3>CRUD機能を試す</h3>
<p>では簡単に実装できたCRUD機能を試してみます。</p>
<p>まずはデータを追加するため、Usersの右側の「+追加」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10.png" alt="" width="2684" height="1588" class="aligncenter size-full wp-image-20164" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10.png 2684w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-10-2048x1212.png 2048w" sizes="auto, (max-width: 2684px) 100vw, 2684px" />
<p>&nbsp;</p>
<p>次に「Uid」、「Last name」、「First name」、「Email」をそれぞれ入力し、画面下の「保存」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11.png" alt="" width="2676" height="1584" class="aligncenter size-full wp-image-20165" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-11-2048x1212.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>これでデータ追加がされたため、一覧画面から対象のデータをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12.png" alt="" width="2682" height="1588" class="aligncenter size-full wp-image-20166" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12.png 2682w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-12-2048x1213.png 2048w" sizes="auto, (max-width: 2682px) 100vw, 2682px" />
<p>&nbsp;</p>
<p>これで追加したデータの詳細が確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13.png" alt="" width="2676" height="1586" class="aligncenter size-full wp-image-20167" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-13-2048x1214.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>次に「Email」を修正し、画面下の「保存」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14.png" alt="" width="2676" height="1584" class="aligncenter size-full wp-image-20168" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-14-2048x1212.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>これでデータの更新ができたので、一覧画面から対象のデータをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15.png" alt="" width="2674" height="1584" class="aligncenter size-full wp-image-20169" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-15-2048x1213.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<p>対象データの詳細画面から「Email」を確認し、更新されていればOKです。</p>
<p>続けて、画面右下の「削除」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16.png" alt="" width="2682" height="1586" class="aligncenter size-full wp-image-20170" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16.png 2682w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-1536x908.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-16-2048x1211.png 2048w" sizes="auto, (max-width: 2682px) 100vw, 2682px" />
<p>&nbsp;</p>
<p>削除の確認画面が表示されるので、「はい、大丈夫です」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17.png" alt="" width="2680" height="1588" class="aligncenter size-full wp-image-20171" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17.png 2680w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-17-2048x1214.png 2048w" sizes="auto, (max-width: 2680px) 100vw, 2680px" />
<p>&nbsp;</p>
<p>これでデータが削除されればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18.png" alt="" width="2684" height="1590" class="aligncenter size-full wp-image-20172" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18.png 2684w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-18-2048x1213.png 2048w" sizes="auto, (max-width: 2684px) 100vw, 2684px" />
<p>&nbsp;</p>
<h3>操作履歴を確認する</h3>
<p>次にメニューに追加しておいた操作履歴を確認してみます。</p>
<p>まずはトップページから「ログエントリー」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19.png" alt="" width="2678" height="1580" class="aligncenter size-full wp-image-20174" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-1024x604.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-768x453.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-1536x906.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-19-2048x1208.png 2048w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>次に削除に関するデータをクリックしてみます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20.png" alt="" width="2674" height="1584" class="aligncenter size-full wp-image-20175" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-20-2048x1213.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<p>これで操作履歴の詳細が確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21.png" alt="" width="2678" height="1592" class="aligncenter size-full wp-image-20176" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-1024x609.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-768x457.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-1536x913.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-2048x1217.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-21-486x290.png 486w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>表示などをカスタマイズして改善する</h2>
<p>上記のようにCRUD機能は簡単に実装できますが、<strong><span style="color: #ff0000;">そのままだと画面表示がイマイチだったり、実務では論理削除が必要になったりする</span></strong>ため、いくつか<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>カスタマイズ</strong></span>してみます。</p>
<p>では管理画面用の設定ファイル「src/myapp/admin.py」を以下のように修正します。</p>
<p>・「src/myapp/admin.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/admin.py"><code>from django.contrib import admin
from django.contrib.admin.models import LogEntry
from django.utils import timezone

from .models import Users

# サイト名の設定
admin.site.site_header = "Django Admin"
admin.site.site_title = "Django Admin"


###############################
# 操作履歴をメニューに追加する設定
###############################


# デフォルトで存在するモデルを使用する
@admin.register(LogEntry)
class LogEntryAdmin(admin.ModelAdmin):
    # 一覧画面に表示するフィールド設定
    list_display = (
        "action_time",
        "user",
        "content_type",
        "object_repr",
        "action_flag",
    )

    # 一覧画面のフィルター設定
    list_filter = ("action_flag", "content_type")

    # 検索用の対象フィールド
    search_fields = ("object_repr", "change_message", "user__username")

    # 変更不可フィールド設定
    readonly_fields = (
        "action_time",
        "user",
        "content_type",
        "object_repr",
        "action_flag",
    )

    # 詳細画面の表示設定
    fieldsets = (
        (
            None,
            {
                "fields": (
                    "action_time",
                    "user",
                    "content_type",
                    "object_repr",
                    "action_flag",
                ),
            },
        ),
    )


###############################
# 共通フィルター用のクラス定義
###############################


# 削除日用のクラス定義
class DeletedAtFilter(admin.SimpleListFilter):
    title = "削除日（deleted_at）"
    parameter_name = "deleted"

    # 選択肢の設定
    def lookups(self, request, model_admin):
        return (
            ("alive", "未削除"),
            ("deleted", "削除済み"),
        )

    # 選択肢に対するクエリ設定
    def queryset(self, request, queryset):
        value = self.value()
        if value == "alive":
            return queryset.filter(deleted_at__isnull=True)
        if value == "deleted":
            return queryset.filter(deleted_at__isnull=False)
        return queryset


###############################
# DBモデルをメニューに追加する設定
###############################


# usersの設定
@admin.register(Users)
class UsersAdmin(admin.ModelAdmin):
    # 一覧画面に表示するフィールド設定
    list_display = [field.name for field in Users._meta.get_fields()]

    # 一覧画面のフィルター設定
    list_filter = ((DeletedAtFilter),)

    # 検索用の対象フィールド
    search_fields = ("id", "uid", "last_name", "first_name", "email")

    # 変更不可フィールド設定
    readonly_fields = ("id", "created_at", "updated_at")

    # 詳細画面の表示設定
    fieldsets = (
        (
            None,
            {
                "fields": (
                    "id",
                    "uid",
                    "last_name",
                    "first_name",
                    "email",
                    "created_at",
                    "updated_at",
                    "deleted_at",
                ),
            },
        ),
    )

    # 単体用の論理削除設定
    def delete_model(self, request, obj):
        obj.updated_at = timezone.now()
        obj.deleted_at = timezone.now()
        obj.save()

    # 一括用の論理削除設定
    def delete_queryset(self, request, queryset):
        queryset.update(updated_at=timezone.now(), deleted_at=timezone.now())</code></pre>
</div>
<p><span style="color: #ff0000;">※削除処理時は論理削除をするように設定</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、フォーマット修正を行います。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run ruff format .
$ docker compose exec django poetry run ruff check . --select I --fix</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、静的コード解析でエラーがでないことを確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec django poetry run ruff check .</code></pre>
</div>
<p>&nbsp;</p>
<h3>CRUD機能を試す</h3>
<p>では再度CRUD機能を試してみます。</p>
<p>まずはデータを追加するため、Usersの右側の「+追加」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22.png" alt="" width="2684" height="1588" class="aligncenter size-full wp-image-20178" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22.png 2684w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-22-2048x1212.png 2048w" sizes="auto, (max-width: 2684px) 100vw, 2684px" />
<p>&nbsp;</p>
<p>次に「Uid」、「Last name」、「First name」、「Email」をそれぞれ入力し、画面下の「保存」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23.png" alt="" width="2676" height="1584" class="aligncenter size-full wp-image-20179" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-23-2048x1212.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>これでデータが追加され、一覧画面からも対象データの詳細を確認できます。</p>
<p>続けて対象データのIDをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24.png" alt="" width="2678" height="1580" class="aligncenter size-full wp-image-20180" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-1024x604.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-768x453.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-1536x906.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-24-2048x1208.png 2048w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>これで追加したデータの詳細が確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25.png" alt="" width="2676" height="1584" class="aligncenter size-full wp-image-20181" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25.png 2676w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-25-2048x1212.png 2048w" sizes="auto, (max-width: 2676px) 100vw, 2676px" />
<p>&nbsp;</p>
<p>次に「Email」を修正し、画面下の「保存」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26.png" alt="" width="2682" height="1586" class="aligncenter size-full wp-image-20182" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26.png 2682w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-1536x908.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-26-2048x1211.png 2048w" sizes="auto, (max-width: 2682px) 100vw, 2682px" />
<p>&nbsp;</p>
<p>これでデータの更新ができたので、一覧画面から対象データのIDをクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27.png" alt="" width="2674" height="1586" class="aligncenter size-full wp-image-20183" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-1024x607.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-768x456.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-1536x911.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-27-2048x1215.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<p>対象データの詳細画面から「Email」を確認し、更新されていればOKです。</p>
<p>続けて、画面右下の「削除」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28.png" alt="" width="2678" height="1592" class="aligncenter size-full wp-image-20184" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-1024x609.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-768x457.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-1536x913.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-2048x1217.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-28-486x290.png 486w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>削除の確認画面が表示されるので、「はい、大丈夫です」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29.png" alt="" width="2674" height="1588" class="aligncenter size-full wp-image-20185" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-1024x608.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-768x456.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-1536x912.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-2048x1216.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-29-486x290.png 486w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<p>これで削除処理ができたので、対象データのDeleted_atに削除日が設定されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30.png" alt="" width="2674" height="1582" class="aligncenter size-full wp-image-20186" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-1536x909.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-30-2048x1212.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<h3>操作履歴を確認する</h3>
<p>次に再度操作履歴を確認してみます。</p>
<p>まずはトップページから「ログエントリー」をクリックします。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31.png" alt="" width="2672" height="1590" class="aligncenter size-full wp-image-20187" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31.png 2672w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-300x179.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-1024x609.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-768x457.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-1536x914.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-2048x1219.png 2048w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-31-486x290.png 486w" sizes="auto, (max-width: 2672px) 100vw, 2672px" />
<p>&nbsp;</p>
<p>これで一覧画面からも詳細が確認できます。</p>
<p>続けて最新の操作履歴をクリックしてみます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32.jpg" alt="" width="2678" height="1582" class="aligncenter wp-image-20188 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32.jpg 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-300x177.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-1024x605.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-768x454.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-1536x907.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-32-2048x1210.jpg 2048w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p>これで操作履歴の詳細が確認できます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33.png" alt="" width="2674" height="1580" class="aligncenter size-full wp-image-20189" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33.png 2674w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-1024x605.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-768x454.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-1536x908.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-33-2048x1210.png 2048w" sizes="auto, (max-width: 2674px) 100vw, 2674px" />
<p>&nbsp;</p>
<h2>グループ機能と管理ユーザー機能について</h2>
<p>詳細は割愛しますが、デフォルトで<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>グループ機能や管理ユーザー機能</strong></span>もついており、これらを使うことで<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>各種権限を付与した管理ユーザーの作成も可能</strong></span>です。</p>
<p><span style="color: #ff0000;">※例えばユーザーテーブルを確認することだけができる管理ユーザーの作成などが可能</span></p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34.png" alt="" width="2678" height="1586" class="aligncenter size-full wp-image-20191" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34.png 2678w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-300x178.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-1024x606.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-768x455.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-1536x910.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/django-admin-34-2048x1213.png 2048w" sizes="auto, (max-width: 2678px) 100vw, 2678px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はPythonのDjangoで管理画面を開発する方法についてまとめました。</p>
<p>実際に試してみると、<span style="color: #3366ff;"><strong>本当に簡単に管理画面を作ることができ、かつシンプルでスマートな感じもめちゃめちゃ良かった</strong></span>です。</p>
<p>私は以前にPHPのLaravel-Adminでの管理画面開発の経験もありますが、それと比較しても作りやすかったので、今後はこれを使っていくべきだなと思いました。</p>
<p>今回は簡単な例をご紹介しましたが、他にも色々カスタマイズはできそうなので、より深掘りしていくのもありかなと思います。</p>
<p>ということで、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>管理画面を作るならPythonのDjangoがおすすめ</strong></span>なので、興味がある方はぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python">PythonのDjangoで管理画面を開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/how-to-develop-an-admin-interface-using-django-in-python/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PythonのFastAPIでDDD（ドメイン駆動設計）構成のバックエンドAPIを開発する方法まとめ</title>
		<link>https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python</link>
					<comments>https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Sat, 06 Dec 2025 00:19:19 +0000</pubDate>
				<category><![CDATA[4. プログラミング]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20088</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 日進月歩で生成AI関連が盛り上がっていますが、将来的にもあらゆるアプリケーションにAI関連機能が組み込まれるのは避けられないでしょう。 そんなA...</p>
The post <a href="https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python">PythonのFastAPIでDDD（ドメイン駆動設計）構成のバックエンドAPIを開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-11.png" alt="" width="672" height="480" class="aligncenter size-full wp-image-20128" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-11.png 672w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-11-300x214.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>日進月歩で生成AI関連が盛り上がっていますが、<strong><span style="color: #ff0000;">将来的にもあらゆるアプリケーションにAI関連機能が組み込まれる</span></strong>のは避けられないでしょう。</p>
<p>そんな<span style="color: #ff0000;"><strong>AI関連機能を作るならPythonを使うのが主軸</strong></span>であり、かつ<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>マイクロサービス</strong></span>として作るなら<strong>「<span style="border-bottom: 2px solid #be3144;">FastAPI</span>」</strong>というフレームワークを使いながら、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>DDD（ドメイン駆動設計）</strong></span>と呼ばれる方法で作られることが多いのではと思います。</p>
<p>そこでこの記事では、PythonのFastAPIでDDD構成のバックエンドAPIを開発する方法についてまとめます。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>PythonのFastAPIでDDD（ドメイン駆動設計）構成のバックエンドAPIを開発する方法まとめ</h2>
<p>まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir fastapi-domain &amp;&amp; cd fastapi-domain
$ mkdir -p deploy/docker/local/py &amp;&amp; touch deploy/docker/local/py/Dockerfile
$ mkdir -p src/myapp &amp;&amp; touch src/myapp/__init__.py src/myapp/main.py
$ touch .env compose.yml</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル開発環境の構築については、いつものようにDockerを利用するため、試したい方は事前にDocker DesktopなどをインストールしてDockerを使える環境を準備して下さい。</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「deploy/docker/local/py/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/py/Dockerfile"><code>FROM python:3.14.0-slim-trixie

# パッケージ管理用のpoetryをインストール
RUN pip3 install --no-cache-dir poetry

# [開発用] 仮想環境を作成
RUN poetry config virtualenvs.in-project true

WORKDIR /py

EXPOSE 9004</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はPythonのバージョン「3.14」を使います。各種ライブラリのパッケージ管理には「poetry」を使います。開発時専用のライブラリを入れるためにpoetryのコンフィグ設定で仮想環境設定「virtualenvs.in-project」を有効化しています。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/main.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/main.py"><code>from fastapi import FastAPI
from fastapi.responses import PlainTextResponse

app = FastAPI()


@app.get("/", response_class=PlainTextResponse)
def root() -&gt; str:
    return "Hello World !!"</code></pre>
</div>
<p>&nbsp;</p>
<p>・「.env」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file=".env"><code>ENV=local
TEST_VALUE=test123</code></pre>
</div>
<p><span style="color: #ff0000;">※ローカル環境用の環境変数ファイル</span></p>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  fastapi:
    container_name: fastapi
    build:
      context: .
      dockerfile: ./deploy/docker/local/py/Dockerfile
    volumes:
      - .:/py
    ports:
      - "9004:9004"
    env_file:
      - .env
    tty: true
    stdin_open: true</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナをビルドします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、パッケージ管理用のpoetryの初期化をします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm fastapi poetry init</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、対話形式で各種設定について聞かれるので、「Package name [py]:」は「fastapi-domain」、「Author [None, n to skip]:」は「n」、「Would you like to define your main dependencies interactively? (yes/no) [yes]」は「no」、「Would you like to define your development dependencies interactively? (yes/no) [yes]」は「no」を入力して実行し、それ以外はそのまま実行して進めます。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1.jpg" alt="" width="1660" height="1016" class="aligncenter wp-image-20094 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1.jpg 1660w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1-300x184.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1-1024x627.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1-768x470.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-1-1536x940.jpg 1536w" sizes="auto, (max-width: 1660px) 100vw, 1660px" />
<p>&nbsp;</p>
<p>完了後、poetryの設定ファイル「pyproject.toml」が作成されます。</p>
<p>次に以下のコマンドを実行し、今回利用する各種パッケージをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose run --rm fastapi poetry add fastapi uvicorn pydantic
$ docker compose run --rm fastapi poetry add --dev ruff mypy</code></pre>
</div>
<p><span style="color: #ff0000;">※FastAPI用の「fastapi」、「uvicorn」、「pydantic」、開発専用ライブラリとして「ruff」（フォーマッター + 静的コード解析）、「mypy」（型チェック）を使います。</span></p>
<p>&nbsp;</p>
<p>次にpoetryの設定ファイル「pyproject.toml」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="pyproject.toml"><code>[project]
name = "fastapi-domain"
version = "0.1.0"
description = ""
authors = [
    {name = "Your Name",email = "you@example.com"}
]
readme = "README.md"
requires-python = "&gt;=3.14"
dependencies = [
    "fastapi (&gt;=0.122.0,&lt;0.123.0)",
    "uvicorn (&gt;=0.38.0,&lt;0.39.0)",
    "pydantic (&gt;=2.12.5,&lt;3.0.0)"
]

# パッケージの読み込み設定
packages = [
    { include = "myapp", from = "src" }
]


[build-system]
requires = ["poetry-core&gt;=2.0.0,&lt;3.0.0"]
build-backend = "poetry.core.masonry.api"

[dependency-groups]
dev = [
    "ruff (&gt;=0.14.7,&lt;0.15.0)",
    "mypy (&gt;=1.19.0,&lt;2.0.0)"
]


# =========================================================
# ruffの設定（フォーマッター + 静的コード解析）
# =========================================================
[tool.ruff]
# Pythonのバージョン指定
target-version = "py314"

# チェック対象外のディレクトリ
exclude = [
    ".venv",
    ".mypy_cache",
    ".ruff_cache",
]

# ==========
# lint設定
# ==========
lint.select = [
    "E", # Pycodestyle エラー
    "F", # Pyflakes
    "B", # bugbear（バグ検出）
    "I", # isort（import整理）
    "UP", # pyupgrade（最新構文）
]

# =========================================================
# mypyの設定（型チェック）
# =========================================================
[tool.mypy]
# Pythonのバージョン指定
python_version = "3.14"

# 厳密モードの有効化
strict = true

# 追加の表示設定
warn_unused_configs = true # 無効な設定や使われていない設定を警告として表示
warn_unreachable = true # 到達不可能コードを警告
show_error_context = true # エラー発生箇所の前後コンテキストを表示
show_column_numbers = true # エラー箇所の列番号を表示

# Pydantic v2の設定
plugins = ["pydantic.mypy"]

# チェック対象外のディレクトリ
exclude = [
    ".venv",
    ".mypy_cache",
    ".ruff_cache",
]

# 外部ライブラリは無視 
ignore_missing_imports = true</code></pre>
</div>
<p><span style="color: #ff0000;">※[project]にpackagesの設定を追加（srcディレクトリのmyappパッケージを読み込む）、ruffとmypyの設定（ほぼ最小構成のはず！）を追加します。</span></p>
<p>&nbsp;</p>
<p>次にファイル「deploy/docker/local/py/Dockerfile」、「compose.yml」をそれぞれ以下のように修正します。</p>
<p>・「deploy/docker/local/py/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/local/py/Dockerfile"><code>FROM python:3.14.0-slim-trixie

# タイムゾーン設定
ENV TZ=Asia/Tokyo

# パッケージ管理用のpoetryをインストール
RUN pip3 install --no-cache-dir poetry

# [開発用] 仮想環境を作成
RUN poetry config virtualenvs.in-project true

WORKDIR /py

# poetry.lockから依存関係をインストール
COPY pyproject.toml poetry.lock .
RUN poetry install --no-root

COPY ./src ./src
EXPOSE 9004</code></pre>
</div>
<p>&nbsp;</p>
<p>・「compose.yml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="compose.yml"><code>services:
  fastapi:
    container_name: fastapi
    build:
      context: .
      dockerfile: ./deploy/docker/local/py/Dockerfile
    command: poetry run uvicorn myapp.main:app --app-dir src --reload --host 0.0.0.0 --port 9004
    volumes:
      - ./pyproject.toml:/py/pyproject.toml
      - ./poetry.lock:/py/poetry.lock
      - ./src:/py/src
    ports:
      - "9004:9004"
    env_file:
      - .env
    tty: true
    stdin_open: true</code></pre>
</div>
<p><span style="color: #ff0000;">※commandでオプション「&#8211;app-dir src」を付与し、「myapp」がトップレベルになるようにしています。これによってimport時のファイルパスが短くなります。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナの再ビルドおよび起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ログ出力を確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose logs</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンド実行後、以下のようにFastAPIの起動に関するログ出力がされていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2.png" alt="" width="1048" height="204" class="aligncenter size-full wp-image-20098" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2.png 1048w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2-300x58.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2-1024x199.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-2-768x149.png 768w" sizes="auto, (max-width: 1048px) 100vw, 1048px" />
<p>&nbsp;</p>
<p>次にブラウザで「http://localhost:9004」を開き、以下のように「Hello World !!」が出力されていればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-3.png" alt="" width="890" height="524" class="aligncenter size-full wp-image-20099" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-3.png 890w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-3-300x177.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-3-768x452.png 768w" sizes="auto, (max-width: 890px) 100vw, 890px" />
<p>&nbsp;</p>
<h3>OpenAPIの仕様書を確認する方法</h3>
<p>FastAPIでは<strong><span style="color: #3366ff;">OpenAPI形式の仕様書が自動で作成される</span></strong>ため、確認したい場合はブラウザで「http://localhost:9004/docs」または「http://localhost:9004/redoc」を開くと確認できます。</p>
<p>・「http://localhost:9004/docs」</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4.png" alt="" width="2690" height="1832" class="aligncenter size-full wp-image-20100" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4.png 2690w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-300x204.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-1024x697.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-768x523.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-1536x1046.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-4-2048x1395.png 2048w" sizes="auto, (max-width: 2690px) 100vw, 2690px" />
<p>&nbsp;</p>
<p>・「http://localhost:9004/redoc」</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5.png" alt="" width="2682" height="1840" class="aligncenter size-full wp-image-20102" srcset="https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5.png 2682w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-300x206.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-1024x703.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-768x527.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-1536x1054.png 1536w, https://tomoyuki65.com/wp-content/uploads/2025/11/fastapi-domain-5-2048x1405.png 2048w" sizes="auto, (max-width: 2682px) 100vw, 2682px" />
<p>&nbsp;</p>
<h3>コード修正後に利用するコマンド</h3>
<p>今回は開発専用ライブラリとして<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「ruff」（フォーマッター + 静的コード解析）、「mypy」（型チェック）を使える</strong></span>ようにしたため、コードを修正した際は必要に応じて以下のコマンドを実行し、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>フォーマット統一</strong></span>および、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>警告やエラーがでていないことを確認</strong></span>して下さい。</p>
<p><strong>・フォーマット修正</strong></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run ruff format .</code></pre>
</div>
<p>&nbsp;</p>
<p><strong>・importの並び替え（フォーマット修正）</strong></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run ruff check . --select I --fix</code></pre>
</div>
<p>&nbsp;</p>
<p><strong>・静的コード解析によるチェック</strong></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run ruff check .</code></pre>
</div>
<p>&nbsp;</p>
<p><strong>・型チェック</strong></p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run mypy .</code></pre>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>共通設定用の各種ファイル作成</h2>
<p>次に以下のコマンドを実行し、共通設定用の各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/infrastructure/config
$ touch src/myapp/infrastructure/__init__.py src/myapp/infrastructure/config/settings.py src/myapp/infrastructure/config/__init__.py
$ mkdir -p src/myapp/infrastructure/logger
$ touch src/myapp/infrastructure/logger/__init__.py src/myapp/infrastructure/logger/logger.py
$ mkdir -p src/myapp/application/context
$ touch src/myapp/application/__init__.py src/myapp/application/context/__init__.py src/myapp/application/context/request_context.py
$ mkdir -p src/myapp/presentation/middleware
$ touch src/myapp/presentation/__init__.py src/myapp/presentation/middleware/__init__.py src/myapp/presentation/middleware/request_middleware.py
$ mkdir -p src/myapp/presentation/exception
$ touch src/myapp/presentation/exception/__init__.py src/myapp/presentation/exception/definitions.py src/myapp/presentation/exception/handlers.py
$ mkdir -p src/myapp/infrastructure/database
$ touch src/myapp/infrastructure/database/__init__.py src/myapp/infrastructure/database/database.py
$ mkdir -p src/myapp/presentation/schema/common
$ touch src/myapp/presentation/schema/__init__.py src/myapp/presentation/schema/common/error.py src/myapp/presentation/schema/common/__init__.py</code></pre>
</div>
<p><span style="color: #ff0000;">※Pythonの場合は、各ディレクトリをパッケージと認識させるための「__init__.py」も作成します。（最近は省略もできるっぽいですが、テストコードなどで影響が出る可能性があったりして、基本的に作った方がいいようです。）</span></p>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/infrastructure/config/settings.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/config/settings.py"><code>from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    env: str = "local"
    # デフォル値を付けない場合は必須になるので注意！
    test_value: str

    model_config = SettingsConfigDict(
        env_file=".env",
    )


# サーバー起動時にインスタンス化
# .envがない場合はOSから環境変数を読み込む
settings = Settings()


def get_settings() -&gt; Settings:
    return settings</code></pre>
</div>
<p><span style="color: #ff0000;">※環境変数を読み込むためのファイルです。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/config/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/config/__init__.py"><code>from .settings import Settings, get_settings

__all__ = ["Settings", "get_settings"]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/logger/logger.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/logger/logger.py"><code>import logging
from logging.config import dictConfig

from myapp.application.context import request_id
from myapp.infrastructure.config.settings import get_settings

settings = get_settings()


class EnvFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -&gt; bool:
        record.env = settings.env
        return True

class RequestIDFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -&gt; bool:
        record.request_id = request_id.get()
        return True


LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "filters": {
        "request_id_filter": {"()": RequestIDFilter},
        "env_filter": {"()": EnvFilter},
    },
    "formatters": {
        "default": {
            "format": (
                "%(asctime)s [%(levelname)s] "
                "ENV=%(env)s - %(name)s "
                "[request_id=%(request_id)s] "
                "%(message)s"
            )
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "filters": ["request_id_filter", "env_filter"],
            "formatter": "default",
        }
    },
    "root": {
        "level": "INFO",
        "handlers": ["console"],
    },
}


def init_logging() -&gt; None:
    dictConfig(LOGGING)


def get_logger(name: str = "fastapi-domain") -&gt; logging.Logger:
    return logging.getLogger(name)</code></pre>
</div>
<p><span style="color: #ff0000;">※ロガーの設定変更が必要な場合はこのファイルを修正して下さい。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/logger/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/logger/__init__.py"><code>from .logger import get_logger, init_logging

__all__ = ["init_logging", "get_logger"]</code></pre>
</div>
<p><span style="color: #ff0000;">※「__init__.py」を設定することで、他のファイルからimportする時のパスを短くできます。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/application/context/request_context.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/context/request_context.py"><code>from contextvars import ContextVar

# リクエストID（リクエスト単位で一意のID）
request_id: ContextVar[str | None] = ContextVar("request_id", default=None)</code></pre>
</div>
<p><span style="color: #ff0000;">※リクエスト単位で一意の値を持たせられるようにするため、コンテキスト変数を作成します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/application/context/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/context/__init__.py"><code>from .request_context import request_id

__all__ = ["request_id"]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/middleware/request_middleware.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/middleware/request_middleware.py"><code>import uuid

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response

from myapp.application.context import request_id
from myapp.infrastructure.logger import get_logger


class RequestMiddleware(BaseHTTPMiddleware):
    async def dispatch(
        self, request: Request, call_next: RequestResponseEndpoint
    ) -&gt; Response:
        # UUIDの取得
        new_uuid = str(uuid.uuid4())

        # リクエストID用コンテキストにUUIDを設定
        request_id.set(new_uuid)

        # レスポンスヘッダーにX-Request-IDを設定
        response = await call_next(request)
        response.headers["X-Request-ID"] = new_uuid

        # リクエスト開始ログ出力
        logger = get_logger()
        logger.info("start request !!")

        return response</code></pre>
</div>
<p><span style="color: #ff0000;">※コンテキストとレスポンスヘッダーにリクエストIDを設定します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/middleware/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/middleware/__init__.py"><code>from .request_middleware import RequestMiddleware

__all__ = ["RequestMiddleware"]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/exception/definitions.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/exception/definitions.py"><code>######################
# 例外定義
######################

# DBエラー
class DatabaseError(Exception):
    def __init__(self, message: str):
        self.message = message
        super().__init__(message)</code></pre>
</div>
<p><span style="color: #ff0000;">※カスタム例外を定義します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/exception/handlers.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/exception/handlers.py"><code>from typing import cast

from fastapi import Request
from fastapi.exception_handlers import request_validation_exception_handler
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

from myapp.infrastructure.logger import get_logger

######################
# 例外用のハンドラー定義
######################


# RequestValidationError用
async def request_validation_error_handler(
    request: Request, exc: Exception
) -&gt; JSONResponse:
    cast_exc = cast(RequestValidationError, exc)
    logger = get_logger()
    logger.warning(f"バリデーションエラー: {cast_exc.errors()}")
    return await request_validation_exception_handler(request, cast_exc)

# バリデーションエラー
async def valid_error_exception_handler(
    request: Request, exc: Exception
) -&gt; JSONResponse:
    return JSONResponse(
        status_code=422,
        content={"detail": str(exc)},
    )


# DBエラー
async def db_error_exception_handler(request: Request, exc: Exception) -&gt; JSONResponse:
    return JSONResponse(
        status_code=500,
        content={"detail": str(exc)},
    )


# 共通エラー
async def general_exception_handler(request: Request, exc: Exception) -&gt; JSONResponse:
    return JSONResponse(
        status_code=500,
        content={"detail": f"Internal Server Error: {str(exc)}"},
    )</code></pre>
</div>
<p><span style="color: #ff0000;">※例外発生時の処理を制御させるためのハンドラー設定です。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/exception/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/exception/__init__.py"><code>from .definitions import DatabaseError
from .handlers import (
    db_error_exception_handler,
    general_exception_handler,
    request_validation_error_handler,
    valid_error_exception_handler,
)

__all__ = [
    "request_validation_error_handler",
    "valid_error_exception_handler",
    "DatabaseError",
    "db_error_exception_handler",
    "general_exception_handler",
]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/database/database.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/database/database.py"><code># 今回はDBインスタンスはダミーとする。
def get_db() -&gt; str:
    return "DBインスタンスのダミー"</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はDBは使わないで進めるため、ダミーとして文字列を返すように定義する。実際にDBを使う場合は、インスタンスを返すようにして下さい。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/database/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/database/__init__.py"><code>from .database import get_db

__all__ = ["get_db"]</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/schema/common/error.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/common/error.py"><code>from pydantic import BaseModel, ConfigDict


# 400 Bad Request
class BadRequestResponse(BaseModel):
    detail: str

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "detail": "Bad Request",
            }
        },
    )


# 401 Unauthorized
class UnauthorizedResponse(BaseModel):
    detail: str

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "detail": "Unauthorized",
            }
        },
    )


# 422 Unprocessable Entity
class UnprocessableEntityResponse(BaseModel):
    detail: str

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "detail": "Unprocessable Entity",
            }
        },
    )


# 500 Internal Server Error
class InternalServerErrorResponse(BaseModel):
    detail: str

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "detail": "Internal Server Error",
            }
        },
    )</code></pre>
</div>
<p><span style="color: #ff0000;">※主にOpenAPI用に共通エラー用のスキーマを定義します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/schema/common/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/common/__init__.py"><code>from .error import (
    BadRequestResponse,
    InternalServerErrorResponse,
    UnauthorizedResponse,
    UnprocessableEntityResponse,
)

__all__ = [
    "BadRequestResponse",
    "UnauthorizedResponse",
    "UnprocessableEntityResponse",
    "InternalServerErrorResponse",
]</code></pre>
</div>
<p>&nbsp;</p>
<h2>DDD（ドメイン駆動設計）のディレクトリ構成について</h2>
<p>この後にDDD（ドメイン駆動設計）でAPIを作成していきますが、ディレクトリ構成としてはDDDの思想に基づいたレイヤードアーキテクチャを採用しています。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-plain" data-lang="Plain Text"><code>/src
 ├── /myapp
 |    ├── /application（アプリケーション層）
 |    |    ├── /context（独立した値を持たせるためのコンテキスト変数）
 |    |    └── /usecase（ユースケース層）
 |    |
 |    ├── /di（依存注入によってユースケースのインスタンスをまとめる）
 |    |
 |    ├── /domain（ドメイン層）
 |    |    ├── entity（ドメインモデルの定義）
 |    |    ├── ValueObject（意味のある値を扱うためのオブジェクト定義）
 |    |    ├── repository（リポジトリのインターフェース定義）
 |    |    └── （仮）service（外部サービスのインターフェース定義）
 |    |
 |    ├── /infrastructure（インフラストラクチャー層）
 |    |    ├── /database（データベース設定）
 |    |    ├── /logger（ロガーの実装）
 |    |    ├── /persistence（リポジトリの実装。DB操作による永続化層。）
 |    |    ├── （仮）/cache（キャッシュを含めたリポジトリの実装。インターフェースはリポジトリと同一。）
 |    |    └── （仮）/externalapi（外部サービスの実装）
 |    |
 |    └── /presentation（プレゼンテーション層）
 |         ├── /exception（カスタム例外定義）
 |         ├── /handler（ハンドラー層）
 |         ├── /middleware（ミドルウェア定義）
 |         ├── /router（ルーター定義）
 |         └── /schema（APIの入出力の仕様を決めるスキーマ定義）
 |
 └── /tests（テストコード用ディレクトリ）
</code></pre>
</div>
<p><span style="color: #ff0000;">※（仮）のものは将来的に追加する想定の例です。</span></p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>Postドメインを例にAPIを作る</h2>
<p>次に以下の手順でPostドメインを例にAPIを作成します。</p>
<p>&nbsp;</p>
<h3>ドメインの定義</h3>
<p>まずは以下のコマンドを実行し、各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir src/myapp/domain/post
$ touch src/myapp/domain/__init__.py src/myapp/domain/post/text.py src/myapp/domain/post/entity.py src/myapp/domain/post/repository.py src/myapp/domain/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/domain/post/text.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/domain/post/text.py"><code># 型ヒントでまだ定義されていないクラス名を文字列として書けるようにする設定
# これによってクラス同士が互いを参照するような場合でも型ヒントが使いやすくなる
from __future__ import annotations

from dataclasses import dataclass


# 値オブジェクトの定義（フィールドは変更不可設定）
@dataclass(frozen=True)
    class Text:
    value: str

def __post_init__(self) -&gt; None:
    ######################
    # バリデーションチェック
    ######################
    if not self.value:
        raise ValueError("テキストの値は必須です。")

    if len(self.value) &gt; 20:
        raise ValueError("テキストの値は20文字以内で入力して下さい。")

    # オブジェクトを文字列として表示する際に呼ばれるメソッド
    # print() や str() を使う時にこのメソッドの戻り値が表示されます
    def __str__(self) -&gt; str:
        return self.value

    # DBからの復元用クラスメソッド（チェック処理無し）
    @classmethod
    def from_raw(cls, raw_value: str) -&gt; Text:
        # __init__を呼ばずにインスタンス作成した値を設定
        obj = cls.__new__(cls)
        object.__setattr__(obj, "value", raw_value)
        return obj</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はPostエンティティ（モデル）のフィールド「text」に意味があることを想定して値オブジェクト（意味のある値を扱うためのオブジェクト）を定義します。新規データ登録時は必ずバリデーションチェックが必要ですが、DB登録後のデータ取得時は基本的にバリデーションチェックは不要なため、復元用のメソッド（from_raw）も定義します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/domain/post/entity.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/domain/post/entity.py"><code># 型ヒントでまだ定義されていないクラス名を文字列として書けるようにする設定
# これによってクラス同士が互いを参照するような場合でも型ヒントが使いやすくなる
from __future__ import annotations

from dataclasses import dataclass

from .text import Text


@dataclass
class Post:
    text: Text
    id: int = 0

    def __post_init__(self) -&gt; None:
    ######################
    # バリデーションチェック
    ######################
    if self.text is None:
        raise ValueError("textは必須です。")

    # DBからの復元用クラスメソッド（チェック処理無し）
    @classmethod
    def from_raw(cls, raw_id: int, raw_text: Text) -&gt; Post:
        # __init__を呼ばずにインスタンス作成した値を設定
        obj = cls.__new__(cls)
        obj.id = raw_id
        obj.text = raw_text
        return obj</code></pre>
</div>
<p><span style="color: #ff0000;">※Postエンティティ（モデル）を定義します。フィールド「text」の型には上記で定義した値オブジェクトを利用します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/domain/post/repository.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/domain/post/repository.py"><code>from abc import ABC, abstractmethod

from .entity import Post


class PostRepository(ABC):
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    @abstractmethod
    async def create(self, db: str, post: Post) -&gt; Post:
        pass

    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    @abstractmethod
    async def find_all(self, db: str) -&gt; list[Post]:
        pass</code></pre>
</div>
<p><span style="color: #ff0000;">※Postモデルのリポジトリ用の抽象クラス（他のクラスに継承し、定義したメソッドを持つことを強制させる設計図のようなもの）を定義します。今回は例として新規データ作成用と全データ取得用の2種類のみ定義します。ユースケース層でトランザクション管理をするため、メソッドでDBインスタンスを渡せる設計にしてますが、今回はDB設定はダミー値を使うのでstr型で設定してます。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/domain/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/domain/post/__init__.py"><code>from .entity import Post
from .repository import PostRepository
from .text import Text

__all__ = ["Post", "PostRepository", "Text"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>リポジトリの実装</h3>
<p>次に以下のコマンドを実行し、リポジトリ実装用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/infrastructure/persistence/post
$ touch src/myapp/infrastructure/persistence/__init__.py src/myapp/infrastructure/persistence/post/post_repo_impl.py src/myapp/infrastructure/persistence/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/infrastructure/persistence/post/post_repo_impl.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/persistence/post/post_repo_impl.py"><code>from myapp.domain.post import Post, PostRepository, Text
from myapp.presentation.exception import DatabaseError


class PostRepositoryImpl(PostRepository):
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    async def create(self, db str, post: Post) -&gt; Post:
        try:
            # DBへデータ登録を完了した想定でpostを返す
            return post

        except Exception as e:
            raise DatabaseError(f"DBエラーが発生しました。: {str(e)}") from e

    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    async def find_all(self, db str) -&gt; list[Post]:
        try:
            # DBからデータを取得した想定で固定値を返す
            posts = [
                Post.from_raw(
                    raw_id=1, raw_text=Text.from_raw(raw_value="Postデータ１")
                ),
                Post.from_raw(
                    raw_id=2, raw_text=Text.from_raw(raw_value="Postデータ２")
                ),
            ]

            return posts

        except Exception as e:
            raise DatabaseError(f"DBエラーが発生しました。: {str(e)}") from e</code></pre>
</div>
<p><span style="color: #ff0000;">※今回はDBを使わないので固定値で返しますが、DBから取得したデータはfrom_rawメソッドを利用してドメインに変換して返します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/infrastructure/persistence/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/infrastructure/persistence/post/__init__.py"><code>from .post_repo_impl import PostRepositoryImpl

__all__ = ["PostRepositoryImpl"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>スキーマの定義</h3>
<p>次に以下のコマンドを実行し、APIの入出力の仕様を決めるスキーマ定義用の各種ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/presentation/schema/post
$ touch src/myapp/presentation/schema/post/request.py src/myapp/presentation/schema/post/response.py src/myapp/presentation/schema/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/presentation/schema/post/request.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/post/request.py"><code>from pydantic import BaseModel, ConfigDict, Field


class PostCreateRequest(BaseModel):
    text: str = Field(..., max_length=20, description="テキスト")

    # OpenAPI用設定
    model_config = ConfigDict(
        # 書き換え不可設定
        frozen=True,
        # 「Example Value」に表示されるJSON
        json_schema_extra={
            "example": {
                "text": "Postテキスト",
            }
        },
    )</code></pre>
</div>
<p><span style="color: #ff0000;">※リクエスト用のスキーマ定義の場合、各種フィールどには「Field()」を使ってバリデーションチェックの条件を付けます。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/schema/post/response.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/post/response.py"><code>from pydantic import BaseModel


class PostResponse(BaseModel):
    id: int
    text: str</code></pre>
</div>
<p><span style="color: #ff0000;">※Postデータをレスポンス結果として返すためのスキーマ定義</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/schema/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/schema/post/__init__.py"><code>from .request import PostCreateRequest
from .response import PostResponse

__all__ = ["PostCreateRequest", "PostResponse"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>ユースケースの定義</h3>
<p>次に以下のコマンドを実行し、ユースケース用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/application/usecase/post
$ touch src/myapp/application/usecase/__init__.py src/myapp/application/usecase/post/create_post.py src/myapp/application/usecase/post/get_posts.py src/myapp/application/usecase/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/application/usecase/post/create_post.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/usecase/post/create_post.py"><code>import logging

from fastapi import HTTPException

from myapp.domain.post import Post, PostRepository, Text
from myapp.presentation.schema.post import PostCreateRequest, PostResponse


class CreatePostUsecase:
    # コンストラクタで依存注入する
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    def __init__(
        self, logger: logging.Logger, db: str, post_repo: PostRepository
    ) -&gt; None:
        self.logger = logger
        self.db = db
        self.post_repo = post_repo

    async def execute(self, req: PostCreateRequest) -&gt; PostResponse:
        try:
            # 新規Postデータ作成
            newPost = Post(text=Text(req.text))

            # DB登録
            post = await self.post_repo.create(self.db, newPost)
            return PostResponse(id=post.id, text=post.text.value)

        # ValueError発生時の例外処理
        except ValueError as e:
            self.logger.warning("バリデーションエラー: %s", e)
            raise HTTPException(
                status_code=422, detail=f"バリデーションエラー: {str(e)}"
            ) from e

        except Exception as e:
            self.logger.error("DB登録に失敗しました。: %s", e)
            raise e</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/application/usecase/post/get_posts.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/usecase/post/get_posts.py"><code>import logging

from myapp.domain.post import PostRepository
from myapp.presentation.schema.post import PostResponse


class GetPostsUsecase:
    # コンストラクタで依存注入する
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    def __init__(
        self, logger: logging.Logger, db: str, post_repo: PostRepository
    ) -&gt; None:
        self.logger = logger
        self.db = db
        self.post_repo = post_repo

    async def execute(self) -&gt; list[PostResponse]:
        try:
            # データ取得
            posts = await self.post_repo.find_all(self.db)
            return [PostResponse(id=p.id, text=p.text.value) for p in posts]

        except Exception as e:
            self.logger.error("DBからのデータ取得に失敗しました。: %s", e)
            raise e</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/myapp/application/usecase/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/application/usecase/post/__init__.py"><code>from .create_post import CreatePostUsecase
from .get_posts import GetPostsUsecase

__all__ = ["CreatePostUsecase", "GetPostsUsecase"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>DIコンテナの作成</h3>
<p>次に以下のコマンドを実行し、依存注入によってユースケースのインスタンスをまとめるためのファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/di
$ touch src/myapp/di/container.py src/myapp/di/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「src/myapp/di/container.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/di/container.py"><code>import logging
from typing import Annotated

from fastapi import Depends

from myapp.application.usecase.post import CreatePostUsecase, GetPostsUsecase
from myapp.domain.post import PostRepository
from myapp.infrastructure.database import get_db
from myapp.infrastructure.logger import get_logger
from myapp.infrastructure.persistence.post import PostRepositoryImpl


# リポジトリのDI用関数
def get_post_repository() -&gt; PostRepository:
    return PostRepositoryImpl()


# Post用ユースケース
def create_post_usecase(
    logger: Annotated[logging.Logger, Depends(get_logger)],
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    db: Annotated[str, Depends(get_db)],
    post_repo: Annotated[PostRepository, Depends(get_post_repository)],
) -&gt; CreatePostUsecase:
    return CreatePostUsecase(logger=logger, db=db, post_repo=post_repo)


def get_posts_usecase(
    logger: Annotated[logging.Logger, Depends(get_logger)],
    # 今回はDB設定はダミー値を使っているため、dbの型はstrにしている
    db: Annotated[str, Depends(get_db)],
    post_repo: Annotated[PostRepository, Depends(get_post_repository)],
) -&gt; GetPostsUsecase:
    return GetPostsUsecase(logger=logger, db=db, post_repo=post_repo)</code></pre>
</div>
<p><span style="color: #ff0000;">※FastAPI用のDependsを使って依存注入しています。今回使ったPythonのバージョンでは「Annotated」を使った依存注入が最適でした。（Pythonのバージョンによってここら辺の書き方が変わるので注意）</span></p>
<p>&nbsp;</p>
<h3>ハンドラーの定義</h3>
<p>次に以下のコマンドを実行し、ハンドラー用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/presentation/handler/post
$ touch src/myapp/presentation/handler/__init__.py src/myapp/presentation/handler/post/post.py src/myapp/presentation/handler/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/presentation/handler/post/post.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/myapp/presentation/handler/post/post.py"><code>from typing import Annotated

from fastapi import Depends

from myapp.application.usecase.post import CreatePostUsecase, GetPostsUsecase
from myapp.di.container import create_post_usecase, get_posts_usecase
from myapp.presentation.schema.post import PostCreateRequest, PostResponse


class CreatePostHandler:
    def __init__(
        self,
        usecase: Annotated[CreatePostUsecase, Depends(create_post_usecase)],
    ):
        self.usecase = usecase

    async def execute(self, req: PostCreateRequest) -&gt; PostResponse:
        return await self.usecase.execute(req)


class GetPostsHandler:
    def __init__(
        self,
        usecase: Annotated[GetPostsUsecase, Depends(get_posts_usecase)],
    ):
        self.usecase = usecase

    async def execute(self) -&gt; list[PostResponse]:
        return await self.usecase.execute()</code></pre>
</div>
<p><span style="color: #ff0000;">※DIコンテナを使ってハンドラーに依存注入して実行します。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/handler/post/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>from .post import CreatePostHandler, GetPostsHandler

__all__ = ["CreatePostHandler", "GetPostsHandler"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>ルーター設定</h3>
<p>次に以下のコマンドを実行し、ルーター設定用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/myapp/presentation/router
$ touch src/myapp/presentation/router/router.py src/myapp/presentation/router/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/myapp/presentation/router/router.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>from typing import Annotated, Any

from fastapi import APIRouter, Depends

from myapp.presentation.handler.post import CreatePostHandler, GetPostsHandler
from myapp.presentation.schema.common import InternalServerErrorResponse
from myapp.presentation.schema.post import PostCreateRequest, PostResponse

router = APIRouter()

# OpenAPI用の共通エラーレスポンス定義
common_error_res: dict[int | str, dict[str, Any]] | None = {
    500: {"description": "サーバーエラー", "model": InternalServerErrorResponse},
}


@router.post(
    "/post",
    response_model=PostResponse,
    status_code=201,
    responses=common_error_res,
    summary="Postデータ新規作成",
    description="Postデータを新規作成する",
    tags=["Post"],
)
async def create_post(
    req: PostCreateRequest,
    handler: Annotated[CreatePostHandler, Depends(CreatePostHandler)],
) -&gt; PostResponse:
    return await handler.execute(req)


@router.get(
    "/posts",
    response_model=list[PostResponse],
    status_code=200,
    responses=common_error_res,
    summary="Postデータ全件取得",
    description="Postデータを全件取得する",
    tags=["Post"],
)
async def get_posts(
    handler: Annotated[GetPostsHandler, Depends(GetPostsHandler)],
) -&gt; list[PostResponse]:
    return await handler.execute()</code></pre>
</div>
<p><span style="color: #ff0000;">※ハンドラーをルーター設定に依存注入して実行します。また@router部分ではOpenAPI用の定義も記述しています。</span></p>
<p>&nbsp;</p>
<p>・「src/myapp/presentation/router/__init__.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>from .router import router

__all__ = ["router"]</code></pre>
</div>
<p>&nbsp;</p>
<h3>main.pyの修正</h3>
<p>次にファイル「src/myapp/main.py」を以下のように修正します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware

from myapp.infrastructure.logger import get_logger, init_logging
from myapp.presentation.exception import (
    DatabaseError,
    db_error_exception_handler,
    general_exception_handler,
    request_validation_error_handler,
    valid_error_exception_handler,
)
from myapp.presentation.middleware import RequestMiddleware
from myapp.presentation.router import router


##########################
# アプリ全体の共通初期化処理
##########################
@asynccontextmanager
async def lifespan(app: FastAPI) -&gt; AsyncIterator[None]:
    ############
    # 起動時処理
    ############
    # ロガーの初期化
    init_logging()

    # サーバー起動ログ出力
    logger = get_logger()
    logger.info("start server !!!")
    # アプリ起動
    yield
    ############
    # 終了時処理
    ############
    # サーバー停止ログ出力
    logger.info("stop server !!!")


#############
# ルーター設定
#############
app = FastAPI(
    lifespan=lifespan,
    title="fastapi-domain API",
    description="FastAPIによるDDD構成のAPIです。",
    version="1.0.0",
    # terms_of_service="https://example.com/terms/",
    # contact={
    # "name": "サポート",
    # "url": "https://example.com/contact/",
    # "email": "support@example.com",
    # },
    # license_info={
    # "name": "Apache 2.0",
    # "url": "https://www.apache.org/licenses/LICENSE-2.0.html",
    # },
)
app.include_router(router)

###################
# ミドルウェアの設定
###################

# 許可したいオリジン
origins = [
    "http://localhost:3000",
]

# CORS設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
)

# リクエスト用ミドルウェア
app.add_middleware(RequestMiddleware)


###################
# 例外ハンドラー設定
###################
app.add_exception_handler(RequestValidationError, request_validation_error_handler)
app.add_exception_handler(DatabaseError, db_error_exception_handler)
app.add_exception_handler(ValueError, valid_error_exception_handler)
app.add_exception_handler(Exception, general_exception_handler)</code></pre>
</div>
<p><span style="color: #ff0000;">※各種共通用設定も記述しています。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、フォーマット修正、静的コード解析、型チェックを行い、警告が出ないことを確認します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run ruff format .
$ docker compose exec fastapi poetry run ruff check . --select I --fix
$ docker compose exec fastapi poetry run ruff check .
$ docker compose exec fastapi poetry run mypy .</code></pre>
</div>
<p>&nbsp;</p>
<h3>Dockerコンテナの再ビルドと起動</h3>
<p>次に以下のコマンドを実行し、Dockerコンテナを再ビルドします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナを起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>PostドメインのAPIを試す</h2>
<p>次に上記で作成したPostドメインのAPIをPostmanを使って試します。</p>
<p>まずはPOSTメソッドで「http://localhost:9004/post」を実行し、下図のようにステータスコード201で想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6.jpg" alt="" width="2536" height="1576" class="aligncenter wp-image-20118 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6.jpg 2536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-1024x636.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-768x477.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-1536x955.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-6-2048x1273.jpg 2048w" sizes="auto, (max-width: 2536px) 100vw, 2536px" />
<p>&nbsp;</p>
<p>次に<span style="color: #ff0000;"><strong>リクエストパラメータ「text」を21文字以上にして再度実行</strong></span>し、バリデーションチェックでエラーになればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7.jpg" alt="" width="2540" height="1572" class="aligncenter size-full wp-image-20119" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7.jpg 2540w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-1024x634.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-768x475.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-1536x951.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-7-2048x1268.jpg 2048w" sizes="auto, (max-width: 2540px) 100vw, 2540px" />
<p>&nbsp;</p>
<p>次にGETメソッドで「http://localhost:9004/posts」を実行し、下図のようにステータスコード200で想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8.jpg" alt="" width="2534" height="1568" class="aligncenter size-full wp-image-20120" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8.jpg 2534w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-1024x634.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-768x475.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-1536x950.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-8-2048x1267.jpg 2048w" sizes="auto, (max-width: 2534px) 100vw, 2534px" />
<p>&nbsp;</p>
<h2>テストコードを追加</h2>
<p>次にテストコードを追加して試しますが、まずは以下のコマンドを実行し、テストコードに必要なライブラリをインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry add --dev pytest pytest-asyncio pytest-mock httpx</code></pre>
</div>
<p>&nbsp;</p>
<p>次にpytestの設定をするため、ファイル「pyproject.toml」の末尾に以下のような設定を追加します。</p>
<p>・「pyproject.toml」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="pyproject.toml"><code>・・・

# =========================================================
# pytestの設定
# =========================================================
[tool.pytest.ini_options]
# パス設定
pythonpath = ["src"]
testpaths = ["src/tests"]

# マーカー設定
markers = [
    "unit: mark a test as a unit test",
    "integration: mark a test as an integration test"
]

</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Dockerコンテナを再ビルドして起動します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、テストコード用のファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p src/tests/domain/post
$ touch src/tests/__init__.py src/tests/domain/__init__.py src/tests/domain/post/test_text.py src/tests/domain/post/test_entity.py src/tests/domain/post/__init__.py
$ mkdir -p src/tests/application/usecase/post
$ touch src/tests/application/__init__.py src/tests/application/usecase/__init__.py src/tests/application/usecase/post/test_create_post.py src/tests/application/usecase/post/test_get_posts.py src/tests/application/usecase/post/__init__.py
$ mkdir -p src/tests/presentation/handler/post
$ touch src/tests/presentation/__init__.py src/tests/presentation/handler/__init__.py src/tests/presentation/handler/post/test_post.py src/tests/presentation/handler/post/__init__.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルをそれぞれ以下のように記述します。</p>
<p>・「src/tests/domain/post/test_text.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/tests/domain/post/test_text.py"><code>import dataclasses

import pytest

from myapp.domain.post import Text


@pytest.mark.unit
class TestText:
    def test_create_text_success(self) -&gt; None:
        text = Text("hello")
        assert text.value == "hello"
        assert str(text) == "hello"

    def test_empty_value_should_raise_error(self) -&gt; None:
        with pytest.raises(ValueError, match="テキストの値は必須です。"):
            Text("")

    def test_over_20_chars_should_raise_error(self) -&gt; None:
        over_text = "a" * 21
        with pytest.raises(
            ValueError, match="テキストの値は20文字以内で入力して下さい。"
        ):
            Text(over_text)

    def test_exactly_20_chars_is_ok(self) -&gt; None:
        t = Text("a" * 20)
        assert t.value == "a" * 20

    def test_frozen_object_cannot_be_modified(self) -&gt; None:
        t = Text("hello")
        with pytest.raises(dataclasses.FrozenInstanceError):
            t.value = "changed" # type: ignore[misc]

    def test_from_raw_should_skip_validation(self) -&gt; None:
        raw = "a" * 100
            t = Text.from_raw(raw)
        assert t.value == raw
        assert isinstance(t, Text)</code></pre>
</div>
<p><span style="color: #ff0000;">※テスト実行時にユニットテストのみを指定できるようにするため、「@pytest.mark.unit」を付けています。mypyの型チェックでのエラーをスキップするため、対象箇所に「# type: ignore[misc]」を付けています。</span></p>
<p>&nbsp;</p>
<p>・「src/tests/domain/post/test_entity.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/tests/domain/post/test_entity.py"><code>import pytest

from myapp.domain.post import Post, Text


@pytest.mark.unit
class TestPost:
    def test_create_post_success(self) -&gt; None:
        text = Text("hello")
        post = Post(text=text, id=1)

        assert post.id == 1
        assert post.text == text
        assert isinstance(post.text, Text)

    def test_missing_text_should_raise_error(self) -&gt; None:
        with pytest.raises(ValueError, match="textは必須です。"):
            Post(text=None) # type: ignore[arg-type]

    def test_default_id_is_zero(self) -&gt; None:
        text = Text("hello")
        post = Post(text=text)

        assert post.id == 0

    def test_from_raw_should_skip_validation(self) -&gt; None:
        raw_text = Text.from_raw("raw-text")
        post = Post.from_raw(raw_id=123, raw_text=raw_text)

        assert isinstance(post, Post)
        assert post.id == 123
        assert post.text == raw_text
        assert post.text.value == "raw-text"</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/tests/application/usecase/post/test_create_post.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/tests/application/usecase/post/test_create_post.py"><code>import pytest
from pytest_mock import MockerFixture

from myapp.application.usecase.post import CreatePostUsecase
from myapp.domain.post import Post, Text
from myapp.presentation.schema.post import PostCreateRequest, PostResponse


@pytest.mark.unit
@pytest.mark.asyncio
class TestCreatePost:
    # 正常系テスト
    async def test_create_post_success(self, mocker: MockerFixture) -&gt; None:
        # ===================
        # モック作成
        # ===================
        mock_logger = mocker.Mock()
        mock_db = mocker.Mock()
        mock_post_repo = mocker.Mock()

        # リポジトリのモック化
        mock_post_return_value = Post(text=Text("Postデータ"))
        mock_post_return_value.id = 1
        mock_post_repo.create = mocker.AsyncMock(return_value=mock_post_return_value)

        # ユースケースのモック化
        usecase = CreatePostUsecase(
            logger=mock_logger, db=mock_db, post_repo=mock_post_repo
        )

        # ===================
        # リクエストデータ作成
        # ===================
        req = PostCreateRequest(text="Postデータ")

        # ===================
        # テスト実行
        # ===================
        res = await usecase.execute(req)

        # ===================
        # 検証
        # ===================
        assert isinstance(res, PostResponse)
        assert res.id == 1
        assert res.text == "Postデータ"

        # 対象のリポジトリが1回呼ばれること
        mock_post_repo.create.assert_awaited_once()</code></pre>
</div>
<p><span style="color: #ff0000;">※非同期関数の場合は「@pytest.mark.asyncio」を付けます。ユニットテストでは各種モック化してテストします。</span></p>
<p>&nbsp;</p>
<p>・「src/tests/application/usecase/post/test_get_posts.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="src/tests/application/usecase/post/test_get_posts.py"><code>import pytest
from pytest_mock import MockerFixture

from myapp.application.usecase.post import GetPostsUsecase
from myapp.domain.post import Post, Text
from myapp.presentation.schema.post import PostResponse


@pytest.mark.unit
@pytest.mark.asyncio
class TestGetPosts:
    # 正常系テスト
    async def test_get_posts_success(self, mocker: MockerFixture) -&gt; None:
        # ===================
        # モック作成
        # ===================
        mock_logger = mocker.Mock()
        mock_db = mocker.Mock()
        mock_post_repo = mocker.Mock()

        # リポジトリのモック化
        mock_post_return_value = [
            Post.from_raw(raw_id=1, raw_text=Text.from_raw(raw_value="Postデータ１")),
            Post.from_raw(raw_id=2, raw_text=Text.from_raw(raw_value="Postデータ２")),
        ]
        mock_post_repo.find_all = mocker.AsyncMock(return_value=mock_post_return_value)

        # ユースケースのモック化
        usecase = GetPostsUsecase(
            logger=mock_logger, db=mock_db, post_repo=mock_post_repo
        )

        # ===================
        # テスト実行
        # ===================
        res = await usecase.execute()

        # ===================
        # 検証
        # ===================
        assert isinstance(res, list)
        assert all(isinstance(item, PostResponse) for item in res)
        assert res[0].id == 1
        assert res[0].text == "Postデータ１"
        assert res[1].id == 2
        assert res[1].text == "Postデータ２"

        # 対象のリポジトリが1回呼ばれること
        mock_post_repo.find_all.assert_awaited_once()</code></pre>
</div>
<p>&nbsp;</p>
<p>・「src/tests/presentation/handler/post/test_post.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python"><code>import pytest
from fastapi.testclient import TestClient

from myapp.main import app

client = TestClient(app)


@pytest.mark.integration
class TestPostAPI:
    def test_create_post_success(self) -&gt; None:
        # テスト実行
        res = client.post(
            "/post",
            json={"text": "Postテキスト"},
        )

        # 検証
        assert res.status_code == 201
        assert res.json() == {"id": 0, "text": "Postテキスト"}

    def test_create_post_valid_error(self) -&gt; None:
        # テスト実行
        res = client.post(
            "/post",
            json={"text": "aaaaabbbbbcccccddddd1"},
        )

        # 検証
        assert res.status_code == 422
        data = res.json()
        assert "String should have at most 20 characters" in data["detail"][0]["msg"]
        assert data["detail"][0]["ctx"]["max_length"] == 20

    def test_get_posts_success(self) -&gt; None:
        # テスト実行
        res = client.get("/posts")

        # 検証
        assert res.status_code == 200
        assert res.json() == [
            {"id": 1, "text": "Postデータ１"},
            {"id": 2, "text": "Postデータ２"},
        ]</code></pre>
</div>
<p><span style="color: #ff0000;">※ハンドラーのテストはインテグレーションテストとして、リクエストを実行してテストします。（もし実際にDB操作がある場合はそれも実行されるため、テスト用のDB設定も必要になります。）</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、ユニットテストを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run pytest -m unit </code></pre>
</div>
<p>&nbsp;</p>
<p>コマンドを実行後、以下のように全てのテストがPASSすればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9.jpg" alt="" width="1530" height="454" class="aligncenter wp-image-20121 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9.jpg 1530w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9-300x89.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9-1024x304.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-9-768x228.jpg 768w" sizes="auto, (max-width: 1530px) 100vw, 1530px" />
<p><span style="color: #ff0000;">※オプション「-m unit」でマーク「unit」が付いたものだけ実行しているため、それ以外のスキップしたテストが「3 deselected」として表示されますが、それは仕様なので気にしなくてOKです。</span></p>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、インテグレーションテストを実行します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry run pytest -m integration</code></pre>
</div>
<p>&nbsp;</p>
<p>コマンドを実行後、以下のように全てのテストがPASSすればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10.jpg" alt="" width="3022" height="374" class="aligncenter wp-image-20122 size-full" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10.jpg 3022w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-300x37.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-1024x127.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-768x95.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-1536x190.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-10-2048x253.jpg 2048w" sizes="auto, (max-width: 3022px) 100vw, 3022px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2><span id="toc15">データベースついて</span></h2>
<p><strong><span style="color: #ff0000;">今回はDB部分は省略</span></strong>しましたが、組み込みたい場合は以前に書いた以下の記事などを参考にしつつ<span style="color: #ff0000;">（生成AIを使っていない時代に書いた記事で情報が古いので参考程度にどうぞ）</span>、<strong><span style="color: #ff0000;">必要な部分を追加修正</span></strong>していけばできると思います。</p>
<p>&nbsp;</p>
<h2>本番環境用Dockerコンテナを作って試す</h2>
<p>次に本番環境へのデプロイを想定し、専用のDockerコンテナを作ってローカル環境で試してみますが、<strong><span style="color: #ff0000;">コンテナ一つで複数ワーカーを動かしたいかどうかで若干やり方が変わる</span></strong>ため、それぞれご紹介します。</p>
<p>まずは以下のコマンドを実行し、本番環境用のDockerfileを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ mkdir -p deploy/docker/prod &amp;&amp; touch deploy/docker/prod/Dockerfile</code></pre>
</div>
<p>&nbsp;</p>
<h3>k8sやCloud Runなどコンテナ単位でスケールする場合</h3>
<p>k8sやCloud Runなどコンテナ単位でスケールする場合、上記までのローカル環境と同様に「Uvicorn」のみで起動させればよいです。</p>
<p>その場合、ファイル「deploy/docker/prod/Dockerfile」は以下のように記述します。</p>
<p>・「deploy/docker/prod/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/prod/Dockerfile"><code>####################
# ビルドステージ
####################
FROM python:3.14.0-slim-trixie AS builder

# poetryをインストール
RUN pip3 install --no-cache-dir poetry

# プラグイン「poetry-plugin-export」を追加
RUN poetry self add poetry-plugin-export

WORKDIR /build

# 依存関係のみコピー
COPY pyproject.toml poetry.lock ./

# 本番環境用の依存関係のみをファイル「requirements.txt」に記述
RUN poetry export -f requirements.txt --without-hashes --only main -o requirements.txt

####################
# 実行ステージ
####################
FROM python:3.14.0-slim-trixie AS runner

# 環境変数設定
ENV ENV=production

# テスト用の環境変数設定（ローカルではコマンドで渡す必要があるため）
ARG TEST_VALUE
ENV TEST_VALUE=${TEST_VALUE}

# タイムゾーン設定
ENV TZ=Asia/Tokyo

WORKDIR /py

# 必要なパッケージをインストール
COPY --from=builder /build/requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

COPY ./src/myapp ./src/myapp

# 非rootユーザーを設定
RUN addgroup --system --gid 1001 appuser &amp;&amp; \
    adduser --system --uid 1001 appuser
USER appuser

EXPOSE 9004

# k8sやCloud Runなどコンテナ単位でスケールする場合
# Uvicornのみで起動させる
CMD ["uvicorn", "myapp.main:app", "--app-dir", "src", "--host", "0.0.0.0", "--port", "9004"]
</code></pre>
</div>
<p><span style="color: #ff0000;">※環境変数「TEST_VALUE」は必須にしているため、ビルドコマンド実行時に値を渡せるようにしています。</span></p>
<p>&nbsp;</p>
<h3>ECSなどでコンテナ一つで複数ワーカーを動かしたい場合</h3>
<p>ECSなどでコンテナ一つで複数ワーカーを動かしたい場合、サーバー起動には「Gunicorn + UvicornWorker」の組み合わせを利用します。</p>
<p>その場合、まずは以下のコマンドを実行し、poetryで「gunicorn」をインストールします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose exec fastapi poetry add gunicorn</code></pre>
</div>
<p>&nbsp;</p>
<p>次に以下のコマンドを実行し、Gunicorn用の設定ファイルを作成します。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>touch gunicorn_conf.py</code></pre>
</div>
<p>&nbsp;</p>
<p>次に作成したファイルを以下のように記述します。</p>
<p>・「gunicorn_conf.py」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-python" data-lang="Python" data-file="gunicorn_conf.py"><code>import os

# サーバー設定
bind = "0.0.0.0:9004"
workers = int(os.getenv("WORKERS", "4"))
worker_class = "uvicorn.workers.UvicornWorker"

# ログ設定
loglevel = "info"
accesslog = "-" # 標準出力に出す
errorlog = "-" # 標準エラー出力に出す</code></pre>
</div>
<p>&nbsp;</p>
<p>次に事前に作成したファイル「deploy/docker/prod/Dockerfile」は以下のように記述します。</p>
<p>・「deploy/docker/prod/Dockerfile」</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-git" data-lang="Git" data-file="deploy/docker/prod/Dockerfile"><code>####################
# ビルドステージ
####################
FROM python:3.14.0-slim-trixie AS builder

# poetryをインストール
RUN pip3 install --no-cache-dir poetry

# プラグイン「poetry-plugin-export」を追加
RUN poetry self add poetry-plugin-export

WORKDIR /build

# 依存関係のみコピー
COPY pyproject.toml poetry.lock ./

# 本番環境用の依存関係のみをファイル「requirements.txt」に記述
RUN poetry export -f requirements.txt --without-hashes --only main -o requirements.txt

####################
# 実行ステージ
####################
FROM python:3.14.0-slim-trixie AS runner

# 環境変数設定
ENV ENV=production

# テスト用の環境変数設定（ローカルではコマンドで渡す必要があるため）
ARG TEST_VALUE
ENV TEST_VALUE=${TEST_VALUE}

# タイムゾーン設定
ENV TZ=Asia/Tokyo

WORKDIR /py

# 必要なパッケージをインストール
COPY --from=builder /build/requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt

COPY ./src/myapp ./src/myapp

# 非rootユーザーを設定
RUN addgroup --system --gid 1001 appuser &amp;&amp; \
    adduser --system --uid 1001 appuser
USER appuser

EXPOSE 9004

# ECSなどでコンテナ一つで複数ワーカーを動かしたい場合
# （Gunicorn + UvicornWorker）の組み合わせ
COPY gunicorn_conf.py .
WORKDIR /py/src
CMD ["gunicorn", "-c", "/py/gunicorn_conf.py", "myapp.main:app"]
</code></pre>
</div>
<p><span style="color: #ff0000;">※環境変数「TEST_VALUE」は必須にしているため、ビルドコマンド実行時に値を渡せるようにしています。</span></p>
<p>&nbsp;</p>
<h3>Dockerコンテナ単体でのビルドと起動</h3>
<p>次に以下のコマンドを実行し、Dockerコンテナのビルドおよび起動をします。</p>
<div class="hcb_wrap">
<pre class="prism line-numbers lang-bash" data-lang="Bash"><code>$ docker compose down
$ docker build --no-cache --build-arg TEST_VALUE='PROD-VALUE' -f ./deploy/docker/prod/Dockerfile -t fastapi-domain-api:latest .
$ docker run -d -p 80:9004 fastapi-domain-api:latest</code></pre>
</div>
<p><span style="color: #ff0000;">※ビルド時に「&#8211;build-arg TEST_VALUE=&#8217;PROD-VALUE&#8217;」で環境変数への値を渡しています。今回はテストなのでタグは「latest」ですが、実際にはバージョンのタグを指定するのでご注意下さい。</span></p>
<p>&nbsp;</p>
<h3>PostmanでAPIを試す</h3>
<p>次にエンドポイントを「http://localhost/」とし、上記と同様にAPIをPostmanで実行して試して下さい。</p>
<p>各種APIを実行し、想定通りの結果になればOKです。</p>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12.jpg" alt="" width="2538" height="1572" class="aligncenter size-full wp-image-20133" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12.jpg 2538w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-300x186.jpg 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-1024x634.jpg 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-768x476.jpg 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-1536x951.jpg 1536w, https://tomoyuki65.com/wp-content/uploads/2025/12/fastapi-domain-12-2048x1269.jpg 2048w" sizes="auto, (max-width: 2538px) 100vw, 2538px" />
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>ドメイン駆動設計（Domain-Driven Design）を学ぶのにおすすめの本「ドメイン駆動設計をはじめよう ―ソフトウェアの実装と事業戦略を結びつける実践技法」</h2>
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/12/ddd260401-1.png" alt="" width="1644" height="1292" class="aligncenter size-full wp-image-20248" srcset="https://tomoyuki65.com/wp-content/uploads/2025/12/ddd260401-1.png 1644w, https://tomoyuki65.com/wp-content/uploads/2025/12/ddd260401-1-300x236.png 300w, https://tomoyuki65.com/wp-content/uploads/2025/12/ddd260401-1-1024x805.png 1024w, https://tomoyuki65.com/wp-content/uploads/2025/12/ddd260401-1-768x604.png 768w, https://tomoyuki65.com/wp-content/uploads/2025/12/ddd260401-1-1536x1207.png 1536w" sizes="auto, (max-width: 1644px) 100vw, 1644px" />
<p>上記ではDDD構成でAPIを開発する方法の例をご紹介しましたが、<span style="color: #ff0000;"><strong>実際に実務でドメイン駆動設計を使うのは非常に難しい</strong></span>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>もし実務で使うことを想定するなら、まずは本などでしっかり学ぶのがおすすめ</strong></span>です。</p>
<p>そんなドメイン駆動設計を学ぶのにおすすめの本としては「<a href="https://amzn.to/4viy7WH" target="_blank" rel="noopener">ドメイン駆動設計をはじめよう ―ソフトウェアの実装と事業戦略を結びつける実践技法</a>」があります。</p>
<p>この本を読むことにより、<strong></strong><span style="background-color: #ffc6d1;"><strong>ドメイン駆動設計とはなんなのかを理解でき、かつこういった知識が無いままドメイン駆動設計をやろうとしても失敗するのがよくわかる</strong></span>ようになります。</p>
<p>私がこの本を読んで思ったこととしては、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ソフトウェアを開発するエンジニアの仕事をしているなら、一度は読んだ方がいい内容</strong></span>だなと思ったので、ぜひ参考にしてみて下さい。</p>
<p><span style="color: #ff0000;">※本の内容は難しいため、この記事でご紹介しているコードの例をイメージしつつ（本の中に出てくるコードの例はC#になってます）、わからないところは適宜AIツールに質問して解説してもらいながら学んでいくと理解を深められます。</span></p>
<p>&nbsp;</p>
<p><strong>関連記事</strong></p>
<div class="btn-wrap aligncenter simple"><a href="https://golang.tomoyuki65.com/domain-driven-design-essentials" target="_blank" rel="noopener">ドメイン駆動設計（Domain-Driven Design / DDD）の本質と大事なこと</a></div>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回はPythonのFastAPIでDDD構成のバックエンドAPIを開発する方法について解説しました。</p>
<p><strong><span style="color: #ff0000;">これから数年間は間違いなくAI関連機能の開発が盛り上がるのは避けられない</span></strong>ため、その際は<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>主にPythonを使ったAPI開発が増える</strong></span>のではと思います。</p>
<p>その際は<span style="color: #3366ff;"><strong>軽量でバランスがいいFastAPIでのAPI開発が増えそう</strong></span>ですが、<strong><span style="color: #ff0000;">実務ではドメイン駆動設計で開発することが多い</span></strong>と思うので、FastAPIでDDD構成のバックエンドAPIを開発したい方はぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python">PythonのFastAPIでDDD（ドメイン駆動設計）構成のバックエンドAPIを開発する方法まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/how-to-develop-api-with-ddd-using-fastapi-in-python/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ITエンジニア採用は難しい！？現役Web系エンジニア視点における問題点と解決策まとめ</title>
		<link>https://tomoyuki65.com/problems-and-solutions-in-recruiting-it-engineers</link>
					<comments>https://tomoyuki65.com/problems-and-solutions-in-recruiting-it-engineers#respond</comments>
		
		<dc:creator><![CDATA[Tomoyuki]]></dc:creator>
		<pubDate>Mon, 15 Sep 2025 12:17:34 +0000</pubDate>
				<category><![CDATA[3. Web系エンジニア]]></category>
		<guid isPermaLink="false">https://tomoyuki65.com/?p=20036</guid>

					<description><![CDATA[<p>&#160; こんにちは。Tomoyuki（@tomoyuki65）です。 私は元システムエンジニアで、現在はWeb系エンジニアの仕事をしていますが、最近色々な転職サイトを使ったりして色々思うところが多いです。 &#038;nbs...</p>
The post <a href="https://tomoyuki65.com/problems-and-solutions-in-recruiting-it-engineers">ITエンジニア採用は難しい！？現役Web系エンジニア視点における問題点と解決策まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></description>
										<content:encoded><![CDATA[<hr />
<img loading="lazy" decoding="async" style="border: 1px solid #a9a9a9;" src="https://tomoyuki65.com/wp-content/uploads/2025/09/engineer-recruitment-1.jpg" alt="" width="672" height="480" class="aligncenter size-full wp-image-20068" srcset="https://tomoyuki65.com/wp-content/uploads/2025/09/engineer-recruitment-1.jpg 672w, https://tomoyuki65.com/wp-content/uploads/2025/09/engineer-recruitment-1-300x214.jpg 300w" sizes="auto, (max-width: 672px) 100vw, 672px" />
<p>&nbsp;</p>
<p>こんにちは。Tomoyuki（<a href="https://x.com/intent/follow?screen_name=tomoyuki65" target="_blank" rel="noopener noreferrer">@tomoyuki65</a>）です。</p>
<p>私は元システムエンジニアで、現在はWeb系エンジニアの仕事をしていますが、最近色々な転職サイトを使ったりして色々思うところが多いです。</p>
<p>&nbsp;</p>
<p>簡単に言うと、<span style="font-size: 18pt;"><em><span style="color: #ff0000;"><strong>「なんでこんなクソみたいなやり方してるの？？？」</strong></span></em></span>ということが多かったりします。</p>
<p>&nbsp;</p>
<p>これにはもちろん<span style="color: #ff0000;">既存の転職サイトにおける構造的な問題も含まれていたり</span>しますが、<span style="color: #ff0000;">そもそも採用活動をテキトウにやっていることが原因なんじゃないかなと思う</span>ことも多いです。</p>
<p>そこでこの記事では、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>現役Web系エンジニアである私の視点から、ITエンジニア採用における問題点とその解決策についてまとめ</strong></span>ておきます。</p>
<p>もしITエンジニア採用に困っている人事関係者や、ITエンジニアについてよくわかっていない経営者の方々は、ぜひ参考にしてみて下さい。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>ITエンジニア採用は難しい！？現役エンジニア視点における問題点と解決策まとめ</h2>
<p>まず現役Web系エンジニアである私の目線から、現在のITエンジニア採用における<span style="color: #ff0000;"><strong>問題点</strong></span>については以下の通りです。</p>
<div class="supplement boader">
<ul>
<li><strong> 大量のテキトウなスカウトメール</strong></li>
<li><strong> 文章内の本人が送っていないという嘘スカウト</strong></li>
<li><strong> 生成AIバレバレの気持ちが入ってないスカウト文</strong></li>
<li><strong> 具体的にどんな人材を募集しているかわからない求人票</strong></li>
<li><strong> 雑用フルスタックエンジニア（笑）の募集</strong></li>
<li><strong> 人気が無いのにフルリモートワーク無し</strong></li>
<li><strong> 人気が無いのにコーディング試験がある</strong></li>
<li><strong> 市場価値を見ていない年収レンジ</strong></li>
<li><strong> マネジメント職は別のロール（役割・仕事）</strong></li>
<li><strong> 技術スタックがレガシー</strong></li>
<li><strong> 優秀なエンジニアの定義を間違えている</strong></li>
<li><strong> エンジニアを採用しても辞めていく問題</strong></li>
<li><strong> プロダクトの魅力を語れないスタートアップ経営者</strong></li>
<li><strong> 人売りIT企業多すぎ問題</strong></li>
<li> <strong>リファレンスチェックという糞システム</strong></li>
</ul>
<p><span style="font-size: 16.48px; letter-spacing: 0.1em;"></div></span></p>
<p>&nbsp;</p>
<h2>問題点1：大量のテキトウなスカウトメール</h2>
<p>転職サイトに登録すると、<strong><span style="color: #ff0000;">大量のスカウトメールが届く</span></strong>ことが多いです。</p>
<p>ただし、本当に<strong><span style="color: #ff0000;">テキトウな文面でスカウトが届く</span></strong>ことが多く、ただ大量にかつテキトウにスカウトメールを送っているだけで、私のプロフィールはちゃんと見てないんだなというのがよくわかり、<strong><span style="color: #ff0000;">とても悲しい気持ちになる</span></strong>ことが多いのも事実です。</p>
<p><strong><span style="color: #ff0000;">このやり方では優秀なエンジニアは採用できないのは明確</span></strong>だと思うのですが、それがわからないような無能な人がエンジニア採用をしているということなのでしょうか？。。。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>スカウトメールを送って、返信がくるのは約3%未満という話を聞いたことがありますが、だから大量にスカウトメール送信する、というバカみたいなことは今すぐ辞めましょう。</p>
<p>多くの場合、それは<strong><span style="color: #ff0000;">さまざまなコスト面で非常に無駄</span></strong>なことをしています。それだけにとどまらず、<strong><span style="color: #ff0000;">企業の信頼を落としてしまっている可能性も高い</span></strong>のを理解しましょう。</p>
<p><strong>有名な某ユニコーン企業さんとかもそういうことをやられていて、企業としてやっていることは魅力なのに、スカウトのやり方がクソすぎて非常に残念な気持ちになりました。</strong></p>
<p>今はインターネットによって<strong><span style="color: #ff0000;">良い口コミも悪い口コミも簡単に広まる世の中</span></strong>なので、<span><strong><span style="color: #ff0000;">数打ち当たるという戦法はリスクが高い</span></strong>です。</span></p>
<p><strong></strong>もし大量にスカウトメールを送る必要があるのであれば、せめて<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>「条件に合致した方に一斉にスカウトを送信させていただいております。他の採用媒体含めて送信させていただいている可能性もあるため、重複していた場合はご了承下さい。」というような文面をちゃんと記述する</strong></span>ようにして下さい。</p>
<p>今の時代、何事も信用が大事なのです。真摯な文面を心がけましょう。</p>
<p>&nbsp;</p>
<h2>問題点2：文章内の本人が送っていないという嘘スカウト</h2>
<p>スカウト文の文面について、<strong><em>「株式会社<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26aa.png" alt="⚪" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26aa.png" alt="⚪" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎のCTOをしているXXです。〜」</em></strong>というようなスカウトが届くことがあるのですが、それに返信した際に<strong><em>「株式会社<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26aa.png" alt="⚪" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26aa.png" alt="⚪" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎の採用担当です。〜」</em></strong>という返答をされることが多々あります。</p>
<p>はい。そうですね。<strong><span style="color: #ff0000;">採用担当の方が社長やCTOなどを語ってスカウト文を送信しているというケースが多々存在する</span></strong>んです。</p>
<p>それでも一応カジュアル面談もしてみたりすることがあるのですが、結局は採用担当者がテキトウにスカウトを送信しているんだなというのがわかり、<strong><span style="color: #ff0000;">とても悲しい気持ちになる</span></strong>ことがよくあります。</p>
<p>こういうやり方をしていると、<strong><span style="color: #ff0000;">「企業からスカウトを送ってきたのにもかかわらず、書類選考されて落ちた。」みたいな結果を招き、それが悪い口コミとして広がる恐れ</span></strong>があります。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong><span style="border-bottom: 2px solid #be3144;">嘘つくのやめろ！！</span></strong></span></p>
<p>&nbsp;</p>
<h2>問題点3：生成AIバレバレの気持ちが入ってないスカウト文</h2>
<p>最近はChatGPTやGeminiなどの生成AIを使うことで、スカウトメールの文面も簡単に作ることができますよね。</p>
<p>そのようなスカウトメールを受け取ることも増えてきたように感じますが、<strong><span style="color: #ff0000;">明らかに生成AIで作った文面を読んだ人の気持ちを考えたことはないのでしょうか？？？？</span></strong></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>私には理解できません。</strong></span></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>例えば好きな人に対してラブレターを送ろうとした場合、生成AIで作った文章をそのまま伝えるのでしょうか？それで相手に自分の気持ちが伝わるのでしょうか？</p>
<p>文章で伝えるにしろ、言葉で伝えるのしろ、<span style="color: #ff0000;"><strong>自分の言葉で伝えるようにしないと、気持ちが伝わるわけがない</strong></span>のです。</p>
<p>そのようなクソみたいな文章を送ってくる相手のことを魅力的に思うことはありません。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>生成AIを使って文章を作るのはとても効率的なのでこれからの時代は積極的に使っていくべきですが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>誰かに何かを伝える際には必ず自分の言葉で書き直した文章にしてから送る</strong></span>ようにして下さい。</p>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>自分の言葉にして送ることで、あなたの気持ちを文章で伝えることが可能</strong></span>になります。それは生成AIや機械にはできない、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>人間にしかできないこと</strong></span>なのです。</p>
<p>&nbsp;</p>
<h2>問題点4：具体的にどんな人材を募集しているかわからない求人票</h2>
<p>私がITエンジニア向けの求人票を見て多々思うこととしては、<strong><span style="color: #ff0000;">非エンジニアの方が作ったと思われるような、具体的なことがちゃんと書かれていない求人票が多い</span></strong>です。</p>
<p>具体的なことをちゃんと書かないことにより、例えば<strong><span style="color: #ff0000;">カジュアル面談の場で質問しなければいけないことが増える</span></strong>ため、企業側と求職者側で無駄なやりとり、無駄な時間が多くなってしまいます。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>求人票に書いておいて欲しいことは以下の通りです。</p>
<div class="supplement boader">
<ul>
<li> <strong>どんな会社で、どんな魅力がある会社かどうか</strong></li>
<li><strong> どんなプロダクトを作っていて、それぞれのプロダクトについてどんな技術スタックで作られているか（プロダクトごとにフロントエンド、バックエンド、インフラは何を使っているか具体的に書いて下さい）</strong></li>
<li><strong> その求人が想定している具体的なポジション（メンバーとかEMとかなどの役割）や、具体的な仕事内容（どんな課題があり、どんなことをして欲しいか）はどんなことか。</strong></li>
<li><strong> 仕事内容における具体的な技術スタックはなにか</strong></li>
<li><strong> その求人における想定年収はどれぐらいを見込んでいるか（年収レンジに幅がある場合、後で求職者側と想定が違っていて内定を出しても辞退される可能性が生じ、時間の無駄になる）</strong></li>
<li><strong> フルリモートワークが可能かどうかの有無、または週何日出社を想定しているのか</strong></li>
<li><strong> 出社が必要な場合の交通費について記述があるか</strong></li>
<li><strong> フレックス勤務（コアタイムの有無）や裁量労働制かどうかの有無</strong></li>
<li><strong> その求人票における求める人物像はどのような人材か（レベル感やカルチャーマッチ面がわかりやすこと）</strong></li>
<li><strong> 開発用PCが選べるかどうか（Web系エンジニアであればMacBookPro 14か16で、USキーボードの選択有無、メモリフルカスタムを希望できるかどうか）</strong></li>
</ul>
</div>
<p><span style="color: #ff0000;">※多数のプロダクトを作っていて、それぞれ異なる技術スタックを使っていた場合、その求人票ではどのプロダクトの仕事で、具体的にどの技術スタックを使うことになるのかがわからないと、そもそも応募ができません。それぐらいエンジニア目線では大事なことです。</span></p>
<p>&nbsp;</p>
<h2>問題点5：雑用フルスタックエンジニア（笑）の募集</h2>
<p>主に中小企業においては少人数で開発することが多く、優秀な<span style="color: #ff0000;"><strong>フルスタックエンジニア</strong></span>を求めることがよくあると思います。</p>
<p>特にそういった時の求人票を見ると、<strong><span style="color: #ff0000;">だいたい「フルスタックエンジニア募集！年収500〜700万円」というような感じで募集</span></strong>がされていることも少なくありません。</p>
<p>ただ優秀なエンジニアがそのような求人票を見て思うことは、<span style="color: #ff0000; font-size: 18pt;"><strong>「あ、雑用の募集ね。。」</strong></span>というのが<span style="font-size: 14pt; color: #ff0000;"><strong>現実</strong></span>です。</p>
<p>それはなぜかというと、フルスタックエンジニアというのはフロントエンドもバックエンドもインフラも全てある程度できることが前提ですが、<strong><span style="color: #ff0000;">それぞれの領域には専門性があり、全ての領域をある一定以上のレベルでやれるような優秀な人材はほとんどいない</span></strong>からです。</p>
<p>仮にそんな人材がいたとしても、<strong><span style="color: #ff0000;">最低年収は1000万円以上になる</span></strong>と思います。本当のフルスタックエンジニアというのはそういった存在なのです。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p><strong><span style="border-bottom: 2px solid #be3144;">まずは安直に「フルスタックエンジニア」という言葉を使うことを辞めましょう。</span></strong></p>
<p>もし業務内容がフルスタックエンジニア的なものを想定しているのであれば、<strong><span style="border-bottom: 2px solid #be3144;">フロントエンド、バックエンド、インフラの全ての領域に携わってくれる<span style="border-bottom: 2px solid #be3144;">ソフトウェアエンジニア</span></span><span style="border-bottom: 2px solid #be3144;"></span></strong>を募集というような言葉に変えて下さい。</p>
<p>またはもしちゃんとしたプロダクトを作っている企業であれば、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>プロダクトエンジニア</strong></span>を募集するというような形でも大丈夫でしょう。</p>
<p>言葉の使い方を間違えると、当たり前ですが笑われることがあります。</p>
<p>&nbsp;</p>
<h2>問題点6：人気が無いのにフルリモートワーク無し</h2>
<p>XなどのSNSではよくフルリモートワークについて燃えたりしていますよね。</p>
<p>コロナ禍ではフルリモートワークが流行っていったものの、最近では有名な大手企業を中心に出社回帰をしているケースも増えたりしています。</p>
<p>そういったニュースが増えてくると、うちの会社でも<strong><span style="color: #ff0000;">フルリモートワークは廃止しようということを考える経営者</span></strong>の方も多いのではないのでしょうか。</p>
<p>別にフルリモートワークを廃止するかどうかはその企業の自由ですが、その際には<span style="color: #ff0000;"><strong>一つ確認しておくべきこと</strong></span>があります。</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>あなたの会社は、何もしなくてもエンジニアからの応募が困らないような、大人気企業なのでしょうか？</strong></span></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong><span style="border-bottom: 2px solid #be3144;">もしここで「はい。」と言えないのであれば、フルリモートワークを廃止すべきではありません。</span></strong></p>
<p>有名で人気の企業であるAmazonなどがフルリモートワークを廃止してもいいのは、廃止してもそこで働きたいと思う人が多く、エンジニア採用に困らないからです。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>エンジニアの種類によってはもちろん出社必須の現場もあると思いますが、Web系エンジニアの仕事においてはその限りではありません。</p>
<p>Web系エンジニアにおける<strong><span style="color: #3366ff;">フルリモートワークというのは福利厚生の一つ</span></strong>であり、<span style="color: #3366ff;"><strong>それがあるかどうかは非常に大きな要素</strong></span>になっています。</p>
<p>もちろん<strong><span style="color: #ff0000;">リモートワークは向き不向き、好き嫌いがある</span></strong>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>最も良いのはフルリモートワークも出社するのも自由に選べること</strong></span>です。</p>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>もしあなたの企業が中小企業であり、かつ人気が無くてエンジニア採用に困っているのであれば、フルリモートワークを導入すべき</strong></span>です。</p>
<p>フルリモートワークを導入することで<strong><span style="color: #3366ff;">地方のエンジニアも採用候補になる</span></strong>ため、そういった点でもメリットがあります。</p>
<p>また、リモートワークに関して<strong><span style="color: #ff0000;">否定的な意見の一つに「サボる人がいる」</span></strong>というのがあったりしますが、<strong><span style="color: #ff0000;">それはまた別のベクトルの話であり、そもそもそんな責任感が無い人材を雇ってしまっている企業の責任</span></strong>です。</p>
<p>フルリモートワーク人材を雇う際は、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>少なからず「真面目で責任感がる人材であるかどうか」はちゃんと見たほうがいい</strong></span>でしょう。</p>
<p>尚、<strong><span style="color: #ff0000;">フルリモートワークではジュニアレベルの人材のマネジメントはほぼ不可能</span></strong>なので、<span style="color: #ff0000;"><strong>例えばもし新卒採用が必要になってきたというような場合においては、対象者が一人前になるまでは出社必須にすべきだし、マネジメントする人にも出社をお願いしなければならなくなる</strong></span>ため、その点は抑えておいて下さい。</p>
<p>&nbsp;</p>
<h2>問題点7：人気が無いのにコーディング試験がある</h2>
<p>エンジニアの採用フローの中には<strong><span style="color: #ff0000;">コーディング試験</span></strong>がある場合があり、特に最近だと某スタートアップ企業がWebコーディング試験のプロダクトを出していたりするので、<strong><span style="color: #ff0000;">ついつい導入してしまった中小企業</span></strong>もあったりするのではないでしょうか。</p>
<p>そんなコーディング試験についてですが、私としては否定的な見解であり、<strong><span style="color: #ff0000;">コーディング試験がある企業については基本的に応募することは避ける</span></strong>ようにしています。</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>実務ができるエンジニア目線としては、コーディング試験は本当にめんどくさいです。時間の無駄です。</strong></span></p>
<p>&nbsp;</p>
<p>そんな私も一度試しに受けてみたことがありますが、実際にやってみて思ったこととして、<strong><span style="color: #ff0000;">「プラットフォームが使いづらい」、「プラットフォーム慣れが必要」、「制限時間が邪魔」、「問題がわかりづらく、実務と関係ない」、「バグみたいな挙動がある」、「慣れている人が有利なだけ」</span></strong>というよう感じでした。</p>
<p>&nbsp;</p>
<p><strong><span style="font-size: 18pt;">正直クソです。こんなものでエンジニアの能力を測り切れるものではありません。</span></strong></p>
<p>&nbsp;</p>
<p>なのでもしあなたの企業がコーディング試験を導入しているなら、<strong><span style="color: #ff0000;">ほんとうにそれは必要なのかをちゃんと検証したほうがいい</span></strong>でしょう。</p>
<p><strong><span style="color: #ff0000;">コーディング試験の導入すべきであるといいことを言って勧めてくる企業があった場合は、基本的にはビジネスとして自分たちの商品を売ってお金を稼ぎたいだけ</span></strong>です。その点は注意しましょう。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>コーディング試験が効果的な場面としては、有名で人気な大手企業があって、エンジニアの応募が多すぎて全ての人の面接などができない場合</strong></span>です。</p>
<p>その場合はコーディング試験で一定以上の点数を取る必要があるという壁を設けることにより、応募者のフィルタリングが可能になるため、そういう場合は導入する価値があると思います。</p>
<p>逆にそういうことではなく、コーディング試験によってエンジニアの能力を測り、優秀なエンジニアを採用したいと思って導入しているような場合は、<span style="color: #ff0000;"><strong>実務ができるエンジニアをフィルタリングしてしまったり、そもそも実務ができるエンジニアの応募が少なくなってしまうというデメリット</strong></span>もあったりするので注意して下さい。</p>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>もしあなたの企業が人気が無くてエンジニア採用に困っている中小企業などである場合、コーディング試験の導入はデメリットが大きいので避けた方がいい</strong></span>です。</p>
<p>&nbsp;</p>
<h2>問題点8：市場価値を見ていない年収レンジ</h2>
<p>求人票には「年収500〜700万円」というような年収レンジが記載されていると思いますが、<strong><span style="color: #ff0000;">業務内容と年収レンジが見合ってないことも多々あり</span></strong>ます。</p>
<p>年収レンジが高すぎる場合は問題ないですが、<strong><span style="color: #ff0000;">逆に低すぎる場合についてはエンジニアの価値を軽視していると捉えられ、応募を避けられる可能性が出てくるので注意</span></strong>しましょう。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>エンジニアといっても<strong><span style="color: #ff0000;">使う技術スタックによって市場価値は様々で、その時々で市場価値も変わる</span></strong>ため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ちゃんと現在の市場価値に見合った金額を提示できているかどうかはしっかり確認すべき</strong></span>です。</p>
<p>&nbsp;</p>
<h2>問題点9：マネジメント職は別のロール（役割・仕事）</h2>
<p>日本のIT企業ではマネジメント職の方が年収が高かったりすることがあったりすると思いますが、<span style="border-bottom: 2px solid #be3144;"><strong>マネジメント職と開発職は別々のロール（役割・仕事）であり、求められることも違う</strong></span>ため、<strong><span style="color: #ff0000;">マネジメント職の方が年収が高いという仕組みになっているのはおかしい</span></strong>です。</p>
<p>もしあなたの企業がそのような仕組みで運用している場合、<strong><span style="color: #ff0000;">優秀で技術があるエンジニアから避けられてしまうデメリット</span></strong>もあるため、そういった仕組みについてもちゃんと考えた方がいいでしょう。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>組織で仕事をしていく場合、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>人材は適材適所で配置していく方が上手く行くのは間違いない</strong></span>ため、開発したい人には開発をやらせるべきだし、マネジメントしたい人にはマネジメントをやらせるべきです。</p>
<p>なので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>開発職とマネジメント職は別のロールとして捉え、それぞれのロールごとに適切なグレードを設けるようにして、開発職とマネジメント職の年収の乖離が大きくならないような仕組みにすべき</strong></span>です。</p>
<p>&nbsp;</p>
<h2>問題点10：技術スタックがレガシー</h2>
<p>エンジニア採用をしている企業の中には、<span style="color: #ff0000;"><strong>プロダクトに使っている技術スタックがレガシーになってしまっていることも多々ある</strong></span>と思います。</p>
<p>エンジニアが求人票を見る際は、<strong><span style="color: #ff0000;">ほぼ必ずモダンな技術スタックを使っているかどうかは見られてしまう</span></strong>ため、<strong><span style="color: #ff0000;">技術スタックがレガシーだと応募者が来ないことも多い</span></strong>のではないでしょうか。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>まず大前提として、<strong><span style="color: #ff0000;">システムは作った後は腐っていきます。</span></strong>作っている時はモダンな技術であったとしても、ほっとけばいずれレガシーな技術になってしまうんです。</p>
<p>そのため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>プロダクトやサービスを長期的に運用していくのであれば、使っている技術スタックがレガシーになりそうなら、タイミングを見計らってモダンな技術にリプレイスするということも必要になる</strong></span>でしょう。</p>
<p>もしそれが様々な条件があってできなくて、それでもエンジニア採用を成功させたいとするならば、少なからず<strong><span style="color: #ff0000;">その企業のエンジニアとして働くことによって提供できることをある程度明確に提示するような形で求人票を作って募集をしていくべき</span></strong>です。</p>
<p>例えば<em>「技術はレガシーだけど、社会的に長期的な運用が求められるシステムに携わることができ、そのエンジニアとして働くことで特定のドメイン知識にも精通できるようになったりでき、かつ能力が高い人なら早く出世もできる環境があります。」</em>というような感じで、<span style="color: #ff0000;"><strong>企業として提供できることをちゃんと求人表に書く</strong></span>ようにしましょう。</p>
<p><strong><span style="color: #ff0000;">もしそれでもエンジニア採用ができない場合、もうそれは淘汰されるしかない</span></strong>ので、諦めて下さい。それが嫌ならシステムのリプレイスが必要です。</p>
<p>&nbsp;</p>
<h2>問題点11：優秀なエンジニアの定義を間違えている</h2>
<p>どんな企業も優秀なエンジニアを採用したいはずですが、私が感じるのは<strong><span style="color: #ff0000;">優秀なエンジニアの定義が間違っている</span></strong>のではということです。</p>
<p>具体的には、過去に有名企業に在籍していた経験があったり、過去の経歴において求めている実務経験があることだったり、そういった<span style="font-size: 14pt;"><strong><span style="color: #ff0000;">過去の経験を見過ぎている</span></strong></span>なという印象です。</p>
<p>もしそういった<strong><span style="color: #ff0000;">経験豊富な人材を採用しようとした場合、そもそもそんな人材は数が少ないのと、採用しようとしても高い年収を提示する必要があり、コスパも悪い可能性</span></strong>があります。</p>
<p>そして優秀であればあるほど、おそらく自分で起業したり、就職するにしてもリファラル採用されることの方が多いはずなので、転職市場に出てこないことがほとんどでしょう。</p>
<p>そもそも<span style="font-size: 14pt;"><strong><span style="border-bottom: 2px solid #be3144;">社会における仕事というのは、答えがまだない問題を解決していくことが本質</span></strong></span>なので、過去に経験があるかどうかは重要ではありません。<strong></strong><span style="border-bottom-width: 2px; border-bottom-style: solid; border-bottom-color: #be3144; font-size: 14pt;"><strong>未知なる問題が発生してもそれを解決できるかどうかの方が重要</strong></span>です。</p>
<p>あとは<span style="color: #ff0000;"><strong>企業の役割の一つとして、社員に対して成長の機会を与えることもある</strong></span>ので、現在何をしているかだったり、将来どうなっていきたいかというような、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>その人の人間性だったり、将来的なポテンシャルの部分に目を向けた方がいい</strong></span>のではないかなと、私は思います。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>私が思う優秀なエンジニアの定義としては、主に以下のような人材です。</p>
<div class="supplement boader">
<ul>
<li> <strong>コミュニケーション能力が高く、周りの人と円滑に仕事ができる</strong><strong>（リーダーポジションの場合、ちゃんとリーダーシップを発揮できる）</strong></li>
<li><strong> 普段から技術的な勉強をしていくのが苦ではない（趣味みたいになっている）</strong></li>
<li><strong> インプットやアウトプットを適宜やっている</strong></li>
</ul>
</div>
<p>まず社会で組織に属して働くことにおいて、<strong></strong><span style="border-bottom-width: 2px; border-bottom-style: solid; border-bottom-color: #be3144; font-size: 14pt;"><strong>最も大事なことはコミュニケーション能力</strong></span>です。</p>
<p>エンジニアの中には技術はあってもコミュニケーション能力が低い人が一定数いると思いますが、そういう人材は残念ながら組織における優秀な人材には該当しません。</p>
<p>特定の領域においては専門性や技術的なことが必須で一番大事になることもあると思いますが、多くの場合はコミュニケーション能力が一番大事であり、その次に技術的な要素になると思います。</p>
<p>次にその人が<span style="color: #ff0000;"><strong>エンジニアの素養があるかどうかは非常に重要</strong></span>です。<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>それを判断する一つの方法としては、「普段から技術的な勉強をしていくのが苦では無く、好きで趣味のようにやっているかどうか」</strong></span>です。</p>
<p>よく<span style="color: #ff0000;"><strong>「エンジニアは土日も勉強しないといけないから大変」</strong></span>みたいなことを言う人がいたりしますが、<strong><span style="color: #ff0000;">そういった考えに共感するような人はエンジニアに向いていない</span></strong>ので採用すべきではありません。</p>
<p>現在の競争社会における<strong><span style="color: #ff0000;">優秀な人材になっていくためには勉強し続けるのは必須</span></strong>であり、<strong><span style="color: #ff0000;">それをするためには自分が好きなことである必要もある</span></strong>ため、エンジニアにとっては普段から趣味のように技術をキャッチアップできるかどうかは非常に大事なことです。</p>
<p>あとはインプットだけでなく、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>アウトプットも定期的にちゃんとやっていることも非常に大事</strong></span>なので、何らかのアウトプット活動をしているような人なのかはちゃんと見るべきです。</p>
<p>上記のような要素を兼ね備えた人材であれば、基本的には自走できる人材だと思うし、何か問題があっても自分で解決していける人だと思うので、まだ経験が浅くても将来的には優秀なエンジニアになっていくでしょう。</p>
<p>&nbsp;</p>
<h2>問題点12：エンジニアを採用しても辞めていく問題</h2>
<p>やっとのことエンジニアを採用できたとしても、<strong><span style="color: #ff0000;">「すぐに辞めてしまう」</span></strong>ということもよく聞いたりしますよね。</p>
<p>優秀な人材であればあるほど常に高みを目指しているため、求める環境が無くなれば辞めて行ってしまうのはしょうがないことです。</p>
<p>それを少しでも食い止めたいということであれば、優秀な人材の声には常に耳を傾け、その人が求めるポジションや役割を会社として適宜提供し続けていくしかないです</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>そもそも、人が会社を辞めるのを無くすことは不可能です。</strong></span></p>
<p>&nbsp;</p>
<p>その点については無駄なことは考えずに諦めましょう。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>人が辞めていくのを無くすのは不可能なので、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>辞めていく時は快く送り出し、辞めた後に「あの会社は働きやすくて良かったな」と思ってもらえるような環境作りに力を入れた方が後々会社のためになる</strong></span>ことが多いと思います。</p>
<p>最近だと<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>アルムナイ制度（退職者との組織的な繋がりを維持し、再雇用や新たな関係構築につなげる仕組み）がある会社も存在する</strong></span>ので、そういった制度を作ったりするような形で頑張って行った方が合理的です。</p>
<p>&nbsp;</p>
<h2>問題点13：プロダクトの魅力を語れないスタートアップ経営者</h2>
<p>とあるスタートアップのカジュアル面談にて、これから作ろうとしているAI関連プロダクトの詳細について聞いたことがありました。</p>
<p>その際には顧客の問題解決のためにこういったものを作りたいというのはよくわかったのですが、<strong><span style="color: #ff0000;">ただ現状想定しているものが単なるツール的なもの（他社プロダクトとの連携を前提としているようなもの）を想定していて、将来的にそのプロダクトをどのようにしていきたいのかという、プロダクトを作っていく魅力の部分について語れない経営者</span></strong>の方がいました。</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>はっきり言うと、それだと優秀なエンジニアが協力してくれることはありません。</strong></span></p>
<p>&nbsp;</p>
<p><strong><span style="color: #ff0000;">優秀なエンジニアの方は基本的に魅力的なプロダクトの開発に携わりたい生き物</span></strong>なので、魅力もないプロダクトを開発するために、わざわざリスクを取ってスタートアップに入社することはないです。</p>
<p>もしそれでもそういったものを作りたいなら、<strong><span style="color: #ff0000;">外部のIT企業にお願いして作ってもらうしかないと思いますが、それだとお金のためにぼったくられることも多かったりするので、いつかどこかで破綻する</span></strong>と思います。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>非エンジニアの経営者で、あまりソフトウェア開発の知識が無いのであれば、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>できるだけ早い段階で技術的に詳しいアドバイザーなどに相談し、作ろうとしているプロダクトに何か問題がないかはチェックしてもらった方がいい</strong></span>です。</p>
<p>&nbsp;</p>
<h2>問題点14：人売りIT企業多すぎ問題</h2>
<p>日本でエンジニアの仕事を探そうとすると、<strong><span style="color: #ff0000;">SIerやSESといったサービスの提供を事業としているIT企業が非常に多い</span></strong>です。</p>
<p>大手企業ならまだ問題ないと思いますが、<strong><span style="color: #ff0000;">多重下請け構造になってしまっている中小企業が非常に多いのも現実</span></strong>です。</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>結局、そういった中小企業が多いのは、経営者だけが甘い汁を吸えるようなビジネスモデルになっているからです。</strong></span></p>
<p>&nbsp;</p>
<p>ただ最近だと生成AIも発達してきて、<strong><span style="color: #ff0000;">非エンジニアの方でもシステム開発のハードルが下がってきているので、そういった中小企業の仕事が無くなっていくのは時間の問題</span></strong>かなと思います。</p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p><strong><span style="color: #ff0000;">生成AIの発達次第で、特に中小企業は案件獲得が厳しくなっていくのが想定される</span></strong>ため、多重下請け構造での稼ぎがメインになっている中小企業はご注意下さい。</p>
<p>はっきり言って<strong><span style="color: #ff0000;">そういったサービスを提供するようなビジネスモデルは時間の切り売りをしているだけでバイトするのと変わらず、優秀なエンジニアほど魅力を感じなくて避けていく</span></strong>ので、優秀なエンジニアの獲得はより厳しくなっていくと思います。</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>自社プロダクトを作る気が全くないIT企業は淘汰されろ！</strong></span></p>
<p>&nbsp;</p>
<h2>問題点15：リファレンスチェックという糞システム</h2>
<p>選考フローの中に<span style="color: #ff0000;"><strong>「リファレンスチェック」</strong></span>というのを導入している企業があります。</p>
<p>このリファレンスチェックというのは、企業が<span style="color: #ff0000;"><strong>採用候補者の前職の上司や同僚など、候補者をよく知る第三者に対して、候補者の働きぶりや実績、そして人物像などを問い合わせるような採用活動</strong><strong>のこと</strong></span>です。</p>
<p>最近だとリファレンスチェック用のSaaSなどもあり、メールや発行したURLなどを共有して回答してもらうような形式で行われています。</p>
<p>これは企業側にとっては採用候補者の第三者目線の評価が聞けるというメリットはあるのもの、<strong><span style="color: #ff0000;">逆に採用候補者側としては過去に一緒の働いたことがある上司や同僚などの誰かにリファレンスチェックへの回答を無償でお願いしなければならない</span></strong>という、<span style="font-size: 24pt;"><strong><span style="color: #ff0000;">心理的ハードルもめちゃめちゃ高いこともしなければならない</span></strong></span>という、<strong><span style="color: #ff0000;"><span style="font-size: 18pt;">非常に糞システム</span>であることが<span style="font-size: 14pt;">最大のデメリット</span></span></strong>です。</p>
<p>いや別に人気企業で採用に困ってないなら導入してもフィルタリング効果があるのでいいですが、そうじゃないのにこれを導入している企業は頭が悪いのではないでしょうか。</p>
<p>&nbsp;</p>
<p><strong>世の中にはこういったことを気軽に頼めるような人脈形成が得意な陽キャの人が沢山いるということなのでしょうか？？</strong></p>
<p>&nbsp;</p>
<p><strong>私はそうじゃない人の方が多いと思うし、これを導入することによって、逆に陽キャ系のキラキラ人材みたいな属性の人しか集まらないのではないでしょうか？？</strong></p>
<p>&nbsp;</p>
<p>別にそれでいいなら問題ないですが。。。</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>これを導入していて、人手不足とか言ってないですよね？？？<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f60a.png" alt="😊" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong></span></p>
<p>&nbsp;</p>
<h3>解決策</h3>
<p>これについても<strong><span style="color: #ff0000;">採用に困っているなら導入すべきではない</span></strong>です。</p>
<p><strong><span style="color: #ff0000;">これがあるだけでそもそも選考に進まないという人も一定数いる</span></strong>ので、自分で自分の首を絞めることになります。</p>
<p>もしどうしても導入したいというなら、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>対応するのが厳しいという人に対しては、無理に強制せず、柔軟な採用フローにするようしっかり検討しておくべき</strong></span>です。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>ITエンジニアの採用方法やコツ</h2>
<p>上記ではITエンジニア採用における様々な問題点や解決方法についてご紹介しましたが、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>ITエンジニア採用を成功させるための基本的な方法</strong></span>についてご紹介しておきます。</p>
<p>少なからず<span style="color: #ff0000;"><strong>以下のことは確実に必要</strong></span>だと思うので、こういうのが出来ていなかったなというのがあれば、ぜひ参考にしてみて下さい。</p>
<div class="supplement boader">
<p><strong>1. その企業の経営戦略的なところからどのようなプロダクトを作り、それにはどんなエンジニアに組織を作るべきで、それにはどのような人材が必要になるかをしっかり検討する</strong></p>
<p>&nbsp;</p>
<p><strong>2. 経営的な部分も含めて、検討した内容をちゃんと人事部などに共有する</strong></p>
<p>&nbsp;</p>
<p><strong>3. 共有された内容を元に、既存のエンジニアチームとの兼ね合いも考慮しながら、どんなポジションの人材を、どれぐらいの人数が必要なのかを明確にする</strong></p>
<p>&nbsp;</p>
<p><strong>4. 採用すべき人材の数などが明確になったら、その企業のカルチャーなども含めて、具体的にどういった人材を採用すべきなのかのペルソナ（あたかも実在する人物であるかのように設定される顧客像）を明確にする</strong></p>
<p>&nbsp;</p>
<p><strong>5.  ペルソナを明確に定義してから、それに合った求人票を作成する</strong></p>
<p>&nbsp;</p>
<p><strong>6. 求人票には具体的な業務内容やそれに必要な技術スタックをちゃんと記載する（想定年収はその時の市場価値をチェックすること）</strong></p>
<p>&nbsp;</p>
<p><strong>7. ペルソナが明確であればそれを判断するために確認すべきことが明確になるため、面接などで必ず聞かなければならない質問リストをまとめ、面接担当者に共有し、面接する人によって聞くことがバラバラにならないようにする</strong></p>
<p>&nbsp;</p>
<p><strong>8. スカウトメールを送る際は、相手のプロフィールをちゃんと確認し、その人にあった内容を自分の言葉で伝えるようにすること</strong></p>
<p>&nbsp;</p>
<p><strong>9. カジュアル面談や面接をする前には、必ず対象者のプロフィールをしっかり確認しておく</strong></p>
<p>&nbsp;</p>
<p><strong>10. カジュアル面談では、必ず自社の魅力を伝える努力をする</strong></p>
<p>&nbsp;</p>
<p><strong>11. 面接時は、コミュニケーション能力（チームで円滑に仕事ができそうな人か）、エンジニアの素養（日頃から技術のキャッチアップを趣味みたいにやっているような人か）は必ず確認する</strong></p>
<p>&nbsp;</p>
<p><strong>12. フルリモートワークの対象者については、「真面目で責任感がある人か」を必ず確認する</strong></p>
</div>
<p>&nbsp;</p>
<p>尚、エンジニア採用のために技術ブログをやっている企業もあったりしますが、<strong><span style="color: #ff0000;">技術ブログはその会社でしか知り得なかった貴重な情報を無料公開し、それによって社会に貢献することで価値を示せる効果はありますが、それが組織作りにおけるアピール機会としてはそれほど効果はない</span></strong>と思います。</p>
<p>もし広報活動をSNSなどで行おうとするのであれば、例えば<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>noteで人の感情に訴えかけるような企業の魅力を伝える記事を書いたり、会社の魅力や社内環境をわかりやすく伝えられるようなYouTube動画を公開したりしていく方が効果的</strong></span>です。</p>
<p>&nbsp;</p>
<h2>ITエンジニアに人気の環境について</h2>
<p>次にエンジニア目線で魅力的に映る企業環境についてご紹介しておきます。社内環境を整える必要がある場合は参考にしてみて下さい。</p>
<div class="supplement boader">
<ul>
<li> <strong>自社サービスのプロダクト開発に携われる</strong></li>
<li><strong> うちの会社はこの技術に強くしていくというようなブランディングをちゃんと考えて組織構築をしている</strong></li>
<li><strong> プロダクトを作る際には、最低限の品質はちゃんと担保して作っているカルチャー（もし品質が悪いものを売ったら、バグや問い合わせに追われていずれ崩壊します）</strong></li>
<li><strong> モダンな技術スタックを使っている、レガシーになりそうならリプレイスも検討する</strong></li>
<li><strong> 年収レンジが市場価値から乖離していない</strong></li>
<li><strong> フルリモートワークが可能</strong></li>
<li><strong> フルリモートワークも出社も社員が自由に選択できる</strong></li>
<li><strong> 会社が最寄駅から近い</strong></li>
<li><strong> 出社する必要がある社員がいる場合、オフィスは綺麗で快適である</strong></li>
<li><strong> 出社した場合の交通費は全てちゃんと支給される</strong></li>
<li><strong> フルフレックス勤務（コアタイム無し）か、フレックス勤務（コアタイムは11時〜15時あたりで設定）が可能</strong></li>
<li><strong> マネジメント職と技術職のロールが分かれていて、それぞれにちゃんとキャリアパスがあること</strong></li>
<li><strong> 上位ポジションに就きたい優秀な人がいた際に、上がるために必要な条件を明確に提示できること（条件を満たしたらちゃんと上がれること）</strong></li>
<li><strong> エンジニアとそれ以外の職でちゃんと評価基準がわかれていること（エンジニアの評価は難しいため一緒にすると破綻します）</strong></li>
<li><strong> 稼働している既存サービスの運用を頑張っているエンジニアもちゃんと評価がされること</strong></li>
<li><strong> エンジニアの評価基準は、単なるPR数などではなく、会社の売り上げに繋がるようなことへの貢献度や、既存サービスを安定的に運営していくための貢献度に関するものになっていること</strong></li>
<li><strong> 働き方（特にリモートワークの有無など）や評価制度についてコロコロ変えないこと、変える必要があるなら情報共有や意見交換をちゃんと行い、慎重に進めること</strong></li>
<li><strong> 開発用PCが選べる（Web系エンジニアならMacBookProの14か16で、USキーボードを選択可能で、メモリフルカスタムも可能なこと）</strong></li>
<li><strong> どんなエンジニア組織を作っていきたいかが明確に定義されており、それを社員にちゃんと共有している文化があること</strong></li>
<li><strong> アルムナイ制度があり、辞めていく社員を快く送り出せるカルチャーがある</strong></li>
</ul>
</div>
<p>&nbsp;</p>
<p>尚、2025年9月時点におけるモダンな技術スタックの例としては以下の通りです。</p>
<div class="supplement boader">
<ul>
<li> <strong>フロントエンド：React.js、Next.js（Reactのフレームワーク）、TypeScript</strong></li>
<li><strong> バックエンド：Go（REST、gRPC）、必要に応じてPythonやRust</strong></li>
<li><strong> アプリ：クロスプラットフォームとしてはReact Native（Expo）かFlutter</strong></li>
<li><strong> インフラ：k8sを利用したマイクロサービス開発をしている、中小企業ならAWSよりGoogle Cloud</strong></li>
</ul>
</div>
<p><span style="color: #ff0000;">※迷ったらメルカリの技術スタックを真似するのがいいです。</span></p>
<p>&nbsp;</p>
<h2>ITエンジニア採用でおすすめの採用媒体（サイト）について</h2>
<p>私は色々な転職サイトを使ってみましたが、2025年9月時点でおすすめの採用媒体（サイト）についてご紹介しておきます。</p>
<p>&nbsp;</p>
<h3>1. 各種SNSによるダイレクトリクルーティング</h3>
<p>私がこれからの時代においてもっとも良い方法として思っているのは、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>各種SNS（X、Facebook、LinkedInなど）を利用したダイレクトリクルーティング</strong></span>です。</p>
<p><strong><span style="color: #ff0000;">対象者を探すのは大変</span></strong>ですが、<span style="color: #3366ff;"><strong>対象者がいれば直接コンタクトを取って交渉できる可能性がある</strong></span>ため、<strong><span style="color: #3366ff;">採用コストを安くできたり、人との繋がりを作っていける活動になる</span></strong>からです。</p>
<p>特にこれからの時代は、よりダイレクトリクルーティングが発展していくのではと思っているので、どうしてもエンジニアが採用できないという企業さんは、一度SNSで人材を探すのを試してみてはいかがでしょうか。</p>
<p>&nbsp;</p>
<h3>2. 転職ドラフト</h3>
<p>実務経験があるエンジニアが使うべき転職サイトで最も合理的なのは、<strong><span style="font-size: 14pt;"><a href="https://job-draft.jp/" target="_blank" rel="noopener">転職ドラフト</a></span></strong>です。</p>
<p><strong><span style="color: #ff0000;">求職者登録には実務経験が必要で審査もあり、プロフィールもしっかり書く必要</span></strong>があります。</p>
<p>そして<strong><span style="color: #ff0000;">企業側がスカウトをする際は、最初に年収提示が必須</span></strong>になって、<strong><span style="color: #ff0000;">スカウト文も丁寧に作って送る必要</span></strong>があるため、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>求職者側と企業側がお互いに合理的なやり取りが可能</strong></span>になっています。</p>
<p>&nbsp;</p>
<h3>3. Findy</h3>
<p><strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>GitHubやブログなどを用いてアウトプット活動をしっかりやっている人がよく使っている</strong></span>のが<a href="https://findy-code.io/" target="_blank" rel="noopener"><span style="font-size: 14pt;"><strong>Findy</strong></span></a>です。</p>
<p>求職者側視点でも<span style="color: #3366ff;"><strong>自社開発系企業が多く、求人を探しやすいなという印象が強い</strong></span>ため、2025年9月時点においては優秀な人材が集まりやすいサイトです。</p>
<p>求職者視点では<strong><span style="color: #3366ff;">サイト自体がモダンでシンプルで使いやすい感じ</span></strong>なので、自走できるような優秀なエンジニアを探している場合は、一度利用してみてはいかがでしょうか。</p>
<p>&nbsp;</p>
<h3>4. ビズリーチ</h3>
<p>実務経験があり、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>大企業出身者やマネジメント職の人材を中心に集めたい</strong></span>なら、<a href="https://www.bizreach.jp/" target="_blank" rel="noopener"><span style="font-size: 14pt;"><strong>ビズリーチ</strong></span></a>がおすすめです。</p>
<p>私も登録しているため、エンジニアも一定数いますが、<strong><span style="color: #ff0000;">エンジニア目線では転職ドラフトやFindyの方が求人を探しやすい</span></strong>ため、<strong><span style="color: #ff0000;">技術力があるエンジニアの採用にはそれほど向いていないので注意</span></strong>です。</p>
<p>&nbsp;</p>
<h3>5. WantedlyやGrenn</h3>
<p>実務経験よりも、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>若手やポテンシャル採用</strong></span>をしたい場合は、<a href="https://www.wantedly.com/" target="_blank" rel="noopener"><span style="font-size: 14pt;"><strong>Wantedly</strong></span></a>や<a href="https://www.green-japan.com/" target="_blank" rel="noopener"><span style="font-size: 14pt;"><strong>Green</strong></span></a>を使って探すのが無難です。</p>
<p>Wantedlyは共感で繋がるビジネスSNS、GreenはIT人材向けの求職サイトですが、<strong><span style="color: #ff0000;">それぞれ低い年収レンジでスカウトが来ることが多い感じ</span></strong>です。</p>
<p>&nbsp;</p>
<p><script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><br />
<ins class="adsbygoogle" style="display: block; text-align: center;" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-9453826382162914" data-ad-slot="5514976097"></ins><br />
<script>
     (adsbygoogle = window.adsbygoogle || []).push({});
</script></p>
<h2>最後に</h2>
<p>今回は現役Web系エンジニアである私の視点から、ITエンジニア採用における問題点とその解決策についてまとめました。</p>
<p>ITエンジニア採用が難しく、苦労している企業さんが非常に多そうですが、</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="font-size: 18pt;"><strong>それはやり方が間違っているだけじゃない？</strong></span></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>と、私は伝えたいです。</p>
<p>今回ご紹介したような考え方などをしっかり持っているようなCTOなどに協力してもらいながら、<strong></strong><span style="border-bottom: 2px solid #be3144;"><strong>経営戦略的なところからエンジニアの組織作りのところまでしっかり検討していくようなやり方をすれば、エンジニア採用に困ることは少なくなっていく</strong></span>と思います。</p>
<p>もし現在エンジニア採用でお困りの人事関係者や経営者の方がいたら、ぜひ今回ご紹介した内容を参考にしてみて下さい。</p>
<p>&nbsp;</p>The post <a href="https://tomoyuki65.com/problems-and-solutions-in-recruiting-it-engineers">ITエンジニア採用は難しい！？現役Web系エンジニア視点における問題点と解決策まとめ</a> first appeared on <a href="https://tomoyuki65.com">エンジニアライブログ</a>.]]></content:encoded>
					
					<wfw:commentRss>https://tomoyuki65.com/problems-and-solutions-in-recruiting-it-engineers/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
