【php8上級/準上級試験】模擬問題解説 問題6. &参照(リファレンス)unset()

「第1回 PHP 8 上級 模擬試験」を解説していきます

本記事ではひたすら下記記事の模擬試験の解説をしていきたいと思います!

第1回 PHP 8 上級 模擬試験
https://study.prime-strategy.co.jp/study/ph8ex1/

解説記事一覧

模擬問題 6

リファレンスに関する説明の中で、誤っているものを1つ選びなさい。
なお、すべてのコードの先頭には下記のコードが書かれているものとする。

declare(strict_types=1);
error_reporting(-1);

PHP のリファレンスを使うと、ふたつの変数が同じ内容を指すようにできる。
そのため、以下のコード

$s = 'string';
$s2 = &$s;
$s2 = $s2 . ' add';
echo $s;

は正しく実行でき、結果は string add となる。

下記のコードの理解がポイントです!

$s2 = &$s;

この行では、$s2$s参照 を代入しています。

つまり、$s2$sエイリアス(別名) となり、$s2 に対する変更は $s にも反映されます(逆も同じ)。

よってこの選択肢は⭕正しいです

PHP のリファレンス渡しを使うと、関数内でその引数を修正可能になる。
リファレンス渡しは、呼び出す側で変数に & を付ける必要がある。
& を付けずに呼び出すと、リファレンス渡しにならない。
そのため、以下のコード

function hoge($s) {
    $s = $s . ' add hoge';
}

$s_hoge = 'string';
hoge($s_hoge);
hoge(&$s_hoge);
var_dump($s_hoge);

は正しく実行でき、結果は string(15) “string add hoge” となる。

関数のリファレンス渡しについて

(✅ そもそも リファレンス渡しとは、関数の引数を 「参照できるものにする」 こと、逆は値渡し:デフォルト)

「呼び出し側で & を付ける必要がある」は誤り
「関数の定義側で & を付ける必要がある」が正しい

つまり選択肢は誤り❌となります

PHPのリファレンス返しを使うと、関数の戻り値にリファレンスを指定する事が出来る。
リファレンス返しは、「関数の定義」と「代入」の双方に & を付ける必要がある。
そのため、以下のコード

class hoge {
    public function &test() {
        return $this->s;
    }

    private $s = 'string';
}

$obj = new Hoge();
$s = &$obj->test();
$s = $s . ' add';
var_dump($obj);

は正しく実行でき、結果は次のとおりとなる。

object(hoge)#1 (1) {
  ["s":"hoge":private]=>
  &string(10) "string add"
}

なお、マニュアルには「パフォーマンスを向上させるためだけの目的でこの機能を用いることはやめてください。そのようなことをしなくても、PHPエンジンが自動的に最適化を行います」と記されている。

PHPのリファレンス返しとは?

「リファレンス返し」 とは、関数の戻り値を 参照として返す 方法。
リファレンス返しを使うと、関数の戻り値を直接変更できるようになる。

<使用手順>

  1. 関数の定義時に & を付ける
public function &test() { ... }
  1. 代入時にも & を付ける
$s = &$obj->test(); // ← 代入時にも & を付ける

問題文を見てみると

$obj に Hoge のインスタンスを作成し、プロパティ $s は ‘string’ となる。

⬇️

$s = &$obj->test();
  • & を付けることで $s は $obj->s の参照になる。
  • つまり、$s を変更すると $obj->s も変更される。

⬇️

$s = $s . ' add';

$s に ‘ add’ を追加すると、$obj->s も変更される。

⬇️

$obj->s は ‘string add’ に変更されている。

よって選択肢は正しい⭕です

リファレンスは、unset を使うことで解除する事が出来る。
そのため、以下のコード

$s = 'string';
$s2 = &$s;
$s2 = $s2 . ' add';
var_dump($s, $s2);
unset($s2);
$s2 = $s2 . ' add2';
var_dump($s, $s2);

を実行すると、次のとおりとなる。

string(10) "string add"
string(10) "string add"
Warning: Undefined variable $s2 in ...
string(10) "string add"
string(5) " add2"

変数への参照と unset() の挙動

下記で参照が解除されます

unset($s2);

つまり「$s への参照」が解除され、$s2 自体が未定義状態(存在しない状態) となります。

⬇️

未定義変数は “” (空文字列) として評価されるため、最終的には $s2 に “” . ‘ add2’ が代入されます。つまり ‘ add2’ が格納されます。

var_dump($s, $s2);

// unset後の出力結果
string(10) "string add"
string(5) " add2"

よって選択肢⭕正解です

【php8上級/準上級試験】模擬問題解説 問題5. __toString() __invoke() __get() __debugInfo()

「第1回 PHP 8 上級 模擬試験」を解説していきます

本記事ではひたすら下記記事の模擬試験の解説をしていきたいと思います!

第1回 PHP 8 上級 模擬試験
https://study.prime-strategy.co.jp/study/ph8ex1/

解説記事一覧

模擬問題 5

メソッドに関する説明の中で、誤っているものを1つ選びなさい。
なお「\」はバックスラッシュに読み替えること。
また、すべてのコードの先頭には下記のコードが書かれているものとする。

declare(strict_types=1);
error_reporting(-1);

__toString() メソッドにより、クラスが文字列に変換される際の動作を決めることができる。
そのため、以下のコード

class Hoge {
    public function __toString() {
        return $this->s;
    }

    private $s = 'string';
}

$obj = new Hoge();
echo 'object: ' . $obj , PHP_EOL;

は正しく実行でき、結果は object: string となる。

また、__toString() メソッドで例外を投げる事は、PHP 7.3 までは出来なかったが、PHP 7.4 からは出来るようになった。
そのため、以下のコード

class Hoge {
    public function __toString() {
        throw new \Exception('*** test string ***');
    }

    private $s = 'string';
}

try {
    $obj = new Hoge();
    echo 'object: ' . $obj, PHP_EOL;
} catch(\Throwable $e) {
    echo $e->getMessage(), PHP_EOL;
}

を実行すると *** test string *** となる。

__toString()について

__toString() メソッドは、「オブジェクトを文字列として扱うときに、どんな文字列にするか」を決めるメソッドです。

例えば、echo したり、文字列と結合したりするときに、このメソッドが自動で呼び出されます。

そのうえで、「PHP 7.4 以降は __toString() でも例外を投げられる」ようになりましたので、

問題文では下記の流れで例外がキャッチされます

  1. $obj文字列として扱おうとしている
  2. __toString()自動的に呼ばれる
  3. __toString() の中で throw new \Exception('*** test string ***'); を実行

よって選択肢は⭕です

__invoke() メソッドは、 スクリプトがオブジェクトを関数としてコールしようとした際にコールされる。
そのため、以下のコード

class Hoge {
    public function __invoke() {
        var_dump($this);
        return 'string';
    }
}

$obj = new Hoge();
$r = $obj();
var_dump($r);

は正しく実行でき、結果は次のとおりとなる。

object(Hoge)#1 (0) {
}
string(6) "string"

__invoke() メソッド

__invoke() メソッドは、 スクリプトがオブジェクトを関数としてコールしようとした際にコールされます。

なので問題だと$objはオブジェクトですが、$obj();関数として呼び出されているので__invokeが呼び出されます

⬇️

var_dump($this)の処理について

そもそも$thisとは

$thisは現在のオブジェクトインスタンスを指す特別な変数

var_dumpでオブジェクトを出力するときのフォーマット

object(クラス名)#オブジェクトID (プロパティの数) { プロパティの内容 }

class Test {}
$obj = new Test();
var_dump($obj);
// 出力: object(Test)#1 (0) { }

var_dumpで文字列を出力するときのフォーマット

object(クラス名)#オブ$str = "hello";
var_dump($str);
// 出力: string(5) "hello"ジェクトID (プロパティの数) { プロパティの内容 }

となるので問題文の通りの出力結果となり選択肢は⭕です

__get() は、アクセス不能 (protected または private) または存在しないプロパティからデータを読み込む際に使用する。
__getStatic() は存在せず、オブジェクトのコンテキスト、静的コンテキストのどちらでも動く。
そのため、以下のコード

class Hoge {
    public function __get(string $name) {
        return "not exist {$name}";
    }
}

$obj = new Hoge();
echo $obj->test, PHP_EOL;
echo Hoge::$test2, PHP_EOL;

は正しく実行でき、結果は次のとおりとなる。

not exist test
not exist test2

__getメソッドとは

__get は下記のケースでアクセスしようとした際に呼び出されます(データを読み込もうとしたとき)

__get が呼び出される場合 1. アクセスできないプロパティへのアクセス時 (private, protected) 2. 存在しないプロパティへのアクセス時

問題文で「静的コンテキストのでも動く」とありますが、間違ってます

__getは インスタンス化されたものに対して アクセスされた場合に呼び出される

Hoge::$test2のような静的なアクセスの場合は__getは呼び出されません。
このようなアクセスではエラーが発生します。

よって選択肢は誤り❌です

インスタンス用 __get() __set() __call() __isset() 静的(static)用 __callStatic()

__debugInfo() メソッドが実装されていると、var_dump() でオブジェクトをダンプするときに出力するプロパティの情報を制御できる。
そのため、以下のコード

class Hoge {
    public function __debugInfo() {
        return [
            's' => $this->s,
            'i' => $this->i,
        ];
    }

    private $pass = 'password';
    private $s = 'string';
    private $i = 999;
}

$obj = new Hoge();
var_dump($obj);

は正しく実行でき、結果は次のとおりとなる。

object(Hoge)#1 (2) {
  ["s"]=>
  string(6) "string"
  ["i"]=>
  int(999)
}

__debugInfo()とは

var_dump()が呼ばれた時のオブジェクトの出力を制御するために使用します。

問題文のようにセキュリティ上重要な情報(パスワードなど)を隠し、表示したい情報だけを選んで返せます!

__debugInfo()がない場合は、アクセス可能なすべてのプロパティが表示されます。

よって選択肢は⭕です

【php8上級/準上級試験】模擬問題解説 問題4. __construct() __destruct __call() __callStatic()

「第1回 PHP 8 上級 模擬試験」を解説していきます

本記事ではひたすら下記記事の模擬試験の解説をしていきたいと思います!

第1回 PHP 8 上級 模擬試験
https://study.prime-strategy.co.jp/study/ph8ex1/

解説記事一覧

模擬問題 4

メソッドに関する説明の中で、誤っているものを1つ選びなさい。
なお、すべてのコードの先頭には下記のコードが書かれているものとする。

declare(strict_types=1);
error_reporting(-1);

declare(strict_types=1);とは指定された型に厳密に従わないとエラーとなります

error_reporting(-1);は全てのエラーを報告します

PHP において、コンストラクタは、__construct() メソッドで実装される。
そのため、以下のコード

class Hoge {
    public function __construct() {
        echo __METHOD__, PHP_EOL;
    }
}
$obj = new Hoge();

は正しく実行でき、結果は Hoge::__construct となる。

なお、コンストラクタを有している場合、親クラスのコンストラクタが暗黙の内にコールされることはない。
そのため、以下のコード

class Hoge {
    public function __construct() {
        echo __METHOD__, PHP_EOL;
    }
}

class Foo extends Hoge{
    public function __construct() {
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Foo();

を実行すると、結果は Foo::__construct となる。

親クラスのコンストラクタを実装する場合には、parent::をコールする事が必要となる。
そのため、以下のコード

class Hoge {
    public function __construct() {
        echo __METHOD__, PHP_EOL;
    }
}

class Foo extends Hoge{
    public function __construct() {
        parent::__construct();
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Foo();

を実行すると、結果は次のとおりとなる。

Hoge::__construct
Foo::__construct

今回の問題でのポイントは子クラスでのコンストラクタの継承についてになります。

そもそも「コンストラクタ」とは

コンストラクタは「最初の設定をする特別な関数」

  • 新しいものを作るときの「最初の設定」
  • 必ず必要な情報を設定できる
  • クラスから実体(インスタンス)を作る時に自動的に動く
クラス User 定義 コンストラクタ定義 必須パラメータ: name, age オブジェクト生成 new User(“John”, 25) 定義された必須パラメータを渡す コンストラクタ実行 自動的に__constructが実行 渡されたパラメータで値を設定 class User { function __construct($name, $age) { // ここで必須パラメータを定義 }

子クラスで新しいコンストラクタを定義すると、親のコンストラクタは自動で呼ばれない

子クラスで新しいコンストラクタを定義した場合コンストラクタを継承する場合でもparent::__construct()の呼び出しが必要になります

親クラス User function __construct($name) { $this->name = $name; } 子クラス BasicUser // コンストラクタなし 自動呼び出し ✓ 子クラス AdminUser function __construct($name, $role){} 自動呼び出しなし ✗ $basic = new BasicUser(“John”); // 親のコンストラクタが自動実行 $admin = new AdminUser(“John”, “admin”); // 要parent::__construct()

今回の問題文は親のコンストラクタの呼び出しもしてあるので、親と子両方のコンストラクタが実行されます。よって選択肢は⭕です

PHP において、デストラクタは、__destruct() メソッドで実装される。
そのため、以下のコード

class Hoge {
    public function __destruct() {
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Hoge();

は正しく実行でき、結果は Hoge::__destruct となる。

なお、デストラクタを有している場合、親クラスのデストラクタは、暗黙の内にコールされる。コールされる順番は「子クラスのデストラクタ → 親クラスのデストラクタ」の順番である。
そのため、以下のコード

class Hoge {
    public function __destruct() {
        echo __METHOD__, PHP_EOL;
    }
}

class Foo extends Hoge{
    public function __destruct() {
        echo __METHOD__, PHP_EOL;
    }
}

$obj = new Foo();

を実行すると、結果は次のとおりとなる。

Foo::__destruct
Hoge::__destruct

選択肢2でのポイントはデストラクタですね

デストラクタとは?

デストラクタは、オブジェクトが破棄(削除)される時に自動的に呼び出されるメソッドです。 PHPでは __destruct() という名前で定義します。

クラス継承において親クラスのデストラクタは自動的に呼び出されます。

コンストラクタとは挙動がちがうので注意ですね

クラスの継承とメソッドの呼び出し順序 親クラス (Parent) • __construct() • __destruct() 子クラス (Child) • __construct() • __destruct() コンストラクタ: 明示的な呼び出しが必要 デストラクタ: 自動的に呼び出される デストラクタの実行順序:子クラス → 親クラス
<?php
class ParentClass {
    public function __destruct() {
        echo "親のデストラクタ実行\n";
    }
}

class ChildClass extends ParentClass {
}

$obj = new ChildClass();

// 実行結果
親のデストラクタ実行

ただし

子クラスでデストラクタの継承がある場合は親のデストラクタは呼び出されません

<?php
class ParentClass {
    public function __destruct() {
        echo "親のデストラクタ実行\n";
    }
}

class ChildClass extends ParentClass {
    public function __destruct() {
        echo "子のデストラクタ実行\n";
    }
}

$obj = new ChildClass();

// 実行結果
子のデストラクタ実行

上記より親クラスのデストラクタが呼び出されず、子クラスのデストラクタのみ実行されます!

選択肢は❌です

__call() マジックメソッドを使うと「アクセス不能なメソッドがオブジェクトのコンテキストで呼び出された時」に、処理を入れる事ができる。
そのため、以下のコード

class Hoge {
    public function __call(string $name, array $arguments) {
        echo "call: {$name}", PHP_EOL;
        var_dump($arguments);
        echo PHP_EOL;
    }
}

$obj = new Hoge();
$obj->test();
$obj->test2(1, '2', [3]);

は正しく実行でき、結果は次のとおりとなる。

call: test
array(0) {
}

call: test2
array(3) {
  [0]=>
  int(1)
  [1]=>
  string(1) "2"
  [2]=>
  array(1) {
    [0]=>
    int(3)
  }
}

__callの動作

__call()未定義のインスタンスメソッドが呼ばれた時に実行されます。

第1引数の$methodには呼び出されたメソッド名が入る 第2引数の$argsには引数が配列として渡されます。

$obj->test(“abc”, 123); $method: “test” $args: [abc, 123] 未定義メソッド呼び出し → __call() が実行される 引数は配列として渡される

よって選択肢は⭕です。

__callStatic() マジックメソッドを使うと「アクセス不能なメソッドが静的コンテキストで呼び出された時」に、処理を入れる事ができる。

そのため、以下のコード

class Hoge {
    public static function __callStatic(string $name, array $arguments) {
        echo "call: {$name}", PHP_EOL;
        var_dump($arguments);
    }
}

Hoge::test(1, '2', [3]);

は正しく実行でき、結果は次のとおりとなる。

call: test
array(3) {
  [0]=>
  int(1)
  [1]=>
  string(1) "2"
  [2]=>
  array(1) {
    [0]=>
    int(3)
  }
}

なお、__callStatic() マジックメソッドが動くのは「静的コンテキストで呼び出された時」だけのため、「オブジェクトのコンテキストで呼び出された時」には動かない。
そのため、以下のコード

class Hoge {
    public static function __callStatic(string $name, array $arguments) {
        echo "call: {$name}", PHP_EOL;
        var_dump($arguments);
    }
}

$obj = new Hoge();
$obj->test();

を実行すると、結果は次のとおりとなる。

Fatal error: Uncaught Error: Call to undefined method Hoge::test() in …

__callStaticの呼び出し

流れとしては下記です

  1. 静的メソッドを呼び出します
  2. PHPはそのメソッドが実際にクラスに存在するかチェックします
  3. メソッドが存在しない場合、自動的に __callStatic が呼び出されます
    注意)
MyClass::nonExistentMethod() メソッドは存在する? No __callStatic($name, $arguments) ① 静的メソッド呼び出し ② メソッド存在チェック ③ __callStaticが実行

__callStaticインスタンス呼び出しではエラー

静的メソッドでない場合は「__callStatic」呼び出しはできません、よって選択肢は⭕です!

インスタンス(静的メソッドでない場合)は__call()を使用します!

メソッド呼び出しの違いと__callStaticの動作 静的呼び出し // 静的呼び出し MyClass::undefinedMethod(); MyClass::staticTest(); __callStatic が呼ばれる (定義されている場合) 正常に処理可能 非静的(インスタンス)呼び出し // インスタンス呼び出し $obj = new MyClass(); $obj->undefinedMethod(); __callStaticは動作しない! 代わりに__call()が必要 (ない場合はエラー) インスタンスメソッド($obj->method())の呼び出しには__call()を使用
【php8上級/準上級試験】模擬問題解説 問題1.~ 3.

「第1回 PHP 8 上級 模擬試験」を解説していきます

本記事ではひたすら下記記事の模擬試験の解説をしていきたいと思います!

第1回 PHP 8 上級 模擬試験
https://study.prime-strategy.co.jp/study/ph8ex1/

解説記事一覧

模擬問題 1

「PHPをインストールする」にあたっての一般的な注意事項のうち、誤っているものを1つ選びなさい。

PHPの最新のコードは、公式サイト https://www.php.net/ から、Downloads https://www.php.net/downloads に遷移すると、Changelogの確認を含めて取得できる。

PHPの「最新以外の(古い)コード」は、公式サイトでの提供は全くしていない。そのため、古いバージョンのコードが必要な場合、別途「非公式の外部サイト」からソースコードを入手する必要がある。

ダウンロードできるソースコードの拡張子は「.tar.bz2」「.tar.gz」「.tar.xz」等があるが、解凍すれば中身のコードは同じものである。

Downloads https://www.php.net/downloads にはファイル指紋(sha256)がついているので、ダウンロードしたら、改竄の確認をするとよい。

模擬問題 1 解説

PHP公式サイト(https://www.php.net/downloads)では、最新版以外のバージョンもダウンロード可能です。選択肢 2 は誤り❌です

模擬問題 2

PHPの変数の型についての記述で、誤っているものを1つ選びなさい。
なお、すべてのコードの先頭には下記のコードが書かれているものとする。

declare(strict_types=1);
error_reporting(-1);

下記はマニュアルから一部引用した内容である。

usort ( array &$array , callable $callback ) : bool

整数型 (integer) は整数 ({…, -2, -1, 0, 1, 2, …} という集合) を扱う。
整数のサイズはプラットフォームに依存するが、-2,147,483,648 ~ 2,147,483,647 (32 bit符号付) または -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 (64 bit符号付) である事が多い。
integer 型の範囲外の数を指定した場合、float として解釈されるため、用途と環境によっては注意が必要である。

コールバック (callback) は PHP 5.4 以降では callable タイプヒントと呼ばれていた。
これには、あらゆるビルドイン関数、ユーザ定義関数、メソッド、静的なクラスメソッドが指定できるが、言語構造 (例:echo や empty、isset 等) は指定が出来ない。
コールバックに「オブジェクトのメソッド」を指定する場合、 配列の 0 番目の要素にオブジェクトを、 そして 1 番目の要素にメソッド名を指定する必要がある。
静的なクラスメソッドを指定する場合は、配列の 0 番目の要素にクラス名を、1 番目の要素にメソッド名を指定する必要がある。或いは ‘ClassName::methodName’ という形式で指定してもよい。
コールバック (callback) が使われる関数には usort() 関数がある。
そのため、以下のコード

class Hoge {
    public static function sort($a, $b) {
        return $a <=> $b;
    }
}

class Foo {
    public function sort($a, $b) {
        return $a <=> $b;
    }
}

$awk = [3, 1, 2];
usort($awk, [Hoge::class, 'sort']);
var_dump($awk);

$awk = [3, 1, 2];
usort($awk, 'Hoge::sort');
var_dump($awk);

$awk = [3, 1, 2];
usort($awk, [new Foo(), 'sort']);
var_dump($awk);

は正しく実行でき、結果は次のとおりとなる。

array(3) { 
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
}

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
}

array(3) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
}

浮動小数点数 (float / double) は、小数を扱う。
浮動小数点数には精度があり、PHP では通常 IEEE 754 倍精度フォーマットが使われる。
浮動小数点数はどうしても誤差を考慮する必要があるため、例えば「小数を直接比較して等しいかどうかを調べてはいけない」という事を理解する必要がある。
そのため、以下のコード

if (0.3 === (0.1 + 0.2)) {
echo “=>ここにはならない”;
} else {
echo “=>こちらになる”;
}
を実行すると、=>こちらになる という結果が返る。

論理型 (boolean) は「真偽値」とも呼ばれ、値は、true か false か null のいずれかになる。
なお、true、false、null の文字は、大文字で書いても小文字で書いてもよい。

模擬問題 2 解説

問題文について

declare(strict_types=1);

  • この設定により、「厳密な型指定モード」が有効になります。
  • 厳密な型指定モードでは、関数やメソッドの引数や戻り値に指定された型に厳密に従う必要があります。たとえば、intが指定されているところにfloatstringを渡すと、PHPはエラーを発生させます。

error_reporting(-1);

  • すべての種類のエラー(警告や通知を含む)を報告する設定です。デバッグ時に有効で、どんな些細な問題も見逃さずに確認できます。
usort ( array &$array , callable $callback ) : bool

usortは、ユーザー定義の比較関数(コールバック関数)を使用して配列をソートする関数です。

ソートが成功した場合はtrueを返し、エラーが発生した場合はfalseを返します。

選択肢 4 について

boolean型は、値としてtrueまたはfalseのどちらかを持つ型です。
null は boolean 型ではなく、null 型に属します。

よって誤り❌です

模擬問題 3

クラスに関する説明の中で、誤っているものを1つ選びなさい。
なお「\」はバックスラッシュに読み替えること。
また、すべてのコードの先頭には下記のコードが書かれているものとする。

declare(strict_types=1);
error_reporting(-1);

三つの特別なキーワード self と parent そして static がクラス定義の内部からプロパティまたはメソッドにアクセスする際に使用される。
そのため、以下のコード

class Hoge {
    public function test_1() {
        echo __METHOD__, PHP_EOL;
    }

    public static function testStatic() {
        echo __METHOD__, PHP_EOL;
    }

    public static function staticCall() {
        self::testStatic();
        static::testStatic();
    }
}

class Foo extends Hoge {
    public function test_1() {
        parent::test_1();
        echo __METHOD__, PHP_EOL;
    }

    public static function testStatic() {
      echo __METHOD__, PHP_EOL;
    }
}

$obj = new Foo();
$obj->test_1();
Foo::staticCall();
を実行すると、結果は次のとおりとなる。

Hoge::test_1
Foo::test_1
Hoge::testStatic
Foo::testStatic

static メソッドはオブジェクトのインスタンスを生成せずにコールするため、疑似変数 $this は、 static として宣言されたメソッドの内部から利用することはできない。
そのため、以下のコード

class Hoge {
    public static function test() {
        var_dump($this);
    }
}

Hoge::test();
を実行すると、結果は次のとおりとなる。

Fatal error: Uncaught Error: Using $this when not in object context in …

static プロパティは、アロー演算子 -> によりオブジェクトからアクセス することはできない。
そのため、以下のコード

class Hoge {
    static $i = 100;

    public function test() {
        echo $this->i;
    }
}

$obj = new Hoge();
$obj->test();
を実行すると、結果は次のとおりとなる。

Notice: Accessing static property Hoge::$i as non static in …
Warning: Undefined property: Hoge::$i in …

抽象クラスから継承する際、親クラスの宣言で abstract としてマークされた全てのメソッドは、子クラスで定義されなければならない。

また、これらのメソッドは同等 (あるいはより緩い制約) の可視性で定義される必要がある。

一方でメソッドのシグネチャ (引数の型と順番) については、必須引数の数は同じである必要があるが、型宣言は異なってもかまわない。

そのため、以下のコード

abstract class Hoge {
    abstract public function __construct(string $s, array $a);
    protected $s;
    protected $a;
}

class Foo extends Hoge {
    public function __construct(string $s, \arrayObject $a) {
        $this->s = $s;
        $this->a = $a;
    }
}

$obj = new Foo(”, new \arrayObject());
var_dump($obj);
は正しく実行でき、結果は次のとおりとなる。

object(Foo)#1 (2) {
[“s”:protected]=>
string(0) “”
[“a”:protected]=>
object(ArrayObject)#2 (1) {
[“storage”:”ArrayObject”:private]=>
array(0) {
}
}
}

模擬問題 3 解説

(問題文)バックスラッシュ(\)の使用について

最初の問題文にあった「\」はバックスラッシュに読み替えること」とありましたが、そもそもバックスラッシュについて説明します。

ざっくりいうと、他の名前空間のクラスを使う場合に必要です。たとえばPHPMailerを使用する下記の場合ですね。

外部ライブラリのクラス // バックスラッシュ必要 ⚠️ $mailer = new \PHPMailer\PHPMailer\PHPMailer(); ※ 他の名前空間のクラスを使用する場合はバックスラッシュ必須

(選択肢1)「selfとstaticの違い」について

self::は呼び出されたクラスを参照し、static::は実行時のクラスを参照します。

ParentClass ChildClass self:: static:: 常にParentを参照 実行時のクラスを参照

上記より選択肢の内容は⭕です

(選択肢2、3)「staticメソッドとthis」について

static メソッドはオブジェクトのインスタンスを生成せずにコールするため、疑似変数 $this は、 static として宣言されたメソッドの内部から利用することはできない。

class Car static function getInfo() { // $this は使用不可 ❌ return self::$info; } インスタンス化せずに使用するため、 $thisは存在しません

同様の理由でstaticプロパティにはオブジェクト演算子(->)を使ってアクセスできない

上記より選択肢2、3は⭕

(選択肢4)「abstract:抽象メソッドの型指定の継承ルール」について

  • 引数: 子クラスではより広い型に変更不可
  • 戻り値: 子クラスではより狭い型に変更可能

つまり子クラスで引数はきびしく、戻り値は緩くなるということですね!

型指定の継承ルール 引数の型 Parent: mixed Child: int ✅ Child: mixed ❌ より狭い型のみ可能 戻り値の型 Parent: int Child: mixed ✅ Child: int ✅ より広い型が可能

というのも下記の理由があります

  1. 引数(入力)を厳格に:
  • データの整合性を保証
  • 予期しない入力を防止
  • バグの早期発見
  1. 戻り値(出力)を柔軟に:
  • 拡張性の確保
  • 新機能追加の容易さ
  • オーバーライドの自由度

選択肢4で上記の型指定のルールを確認すると引数の型が「ArrayObjectとarray」で異なっているため誤り❌です

【WordPressプラグイン】プラグインSCF(Smart Custom Fields)の使い方とオプションページの設定方法

SCF(Smart Custom Fields)とは?

**SCF(Smart Custom Fields)**は、WordPressサイトに高度なカスタムフィールド機能を追加するためのプラグインです。これにより、投稿やページ、カスタム投稿タイプに対して多様なデータを簡単に追加・管理することができます。SCFは、その柔軟性と使いやすさから、初心者から上級者まで幅広いユーザーに支持されています。

SCFの導入方法

SCFを導入する手順は以下の通りです:

  1. プラグインのインストール
    • WordPress管理画面にログイン。
    • 「プラグイン」→「新規追加」をクリック。
    • 検索バーに「Smart Custom Fields」と入力し、プラグインを見つけます。
    • 「今すぐインストール」をクリックし、インストール後に「有効化」します。
  2. 基本設定
    • プラグインを有効化すると、管理メニューに「SCF」という項目が追加されます。
    • 「SCF」→「フィールドグループ」から新しいフィールドグループを作成します。

SCFのカスタムフィールド設定項目(説明言い換え版)

項目内容
繰り返しフィールドを複数回使用する必要がある場合にONに設定します。
タイプ(必須)入力させたいデータの形式を選択します。テキスト、画像、日付など多様なオプションから選べます。
ラベルカスタムフィールドの内容を簡潔に表現する名称を入力します。例として「価格」や「写真」などが挙げられます。
名前(必須)フィールドを表示する際に使用する識別子で、半角英数字およびハイフン(-)アンダースコア(_)のみで設定します。例:price, recipe_image
デフォルトフィールドに初期値を設定したい場合に入力します。ユーザーが入力しなくてもこの値が自動的に表示されます。
手順(※)特定の手順が既に決まっている場合、その内容をここに記載します。例:調理手順のステップバイステップ説明。
メモ(※)補足情報や注意事項を記入するフィールドです。例:追加の説明や特記事項など。

(実例:レシピ編)カスタム投稿タイプを作成しカスタムフィールドを適応

カスタム投稿タイプを作成

「Custom Post Type UI」プラグインを使用するか、functions.phpに記述するか2パターンあります

Aパターン「Custom Post Type UI」プラグインを使用

下記の通りカスタム投稿タイプを作成

Bパターン「functions.php」でカスタム投稿タイプを作成

functions.php

<?php

add_action('init', 'custom_posttype');
function custom_posttype()
{
	register_post_type('recipe', array(
		'labels' => array(
			'name' => 'レシピ', // 管理画面のメニューに表示される名前
			'singular_name' => 'レシピ', // 管理画面のメニューに表示される名前(単数形)
		),
		'public' => true, // 一般公開(フロント)・管理画面共に利用
		'show_ui' => true, // 管理画面にメニューを表示
		'rewrite' => true, // WordPress側でパーマリンクが自動で設定される、例えば「http://example.com/recipe/recipe-name」
		'has_archive' => true, // アーカイブページを持つ
		'hierarchical' => true, // 固定ページのように親子関係を持つ
		'menu_position' => 5, // 管理画面のメニューの位置
		'supports' => array('title'), // タイトルのみ(他はカスタムフィールドで設定)
		'show_in_rest' => true, // REST API、Gutenbergでの表示を有効にする
	));
}

「SCR」側の設定で「表示条件」で作成したカスタム投稿タイプを有効化

カスタム投稿タイプ用のテンプレートファイルを作成

WordPress では「single-{投稿タイプのスラッグ}.php」という名前のテンプレートファイルがあると、
その投稿タイプの単一投稿ページ(個別ページ)を表示する際に使われるようになります。

パーマリンクのリライトルールが未更新(リライトルールの再設定が必要)

カスタム投稿タイプの登録コードを変更したとき、あるいは新規に追加したときには
設定画面 > パーマリンク設定 を一度開いて「変更を保存」する必要があります。
(実際には押すだけでOK。これでリライトルールが再生成されます)

SCFでオプションページを作成する方法

WordPress管理画面で「Smart Custom Fields」の新規作成で下記の通りカスタムフィールドを作成

今回は「よくある質問(FAQ)」コンテンツ作成したいので、繰り返しを有効化しました

functions.phpに下記コードを追加

<?php
// よくある質問 オプションページ追加
SCF::add_options_page('よくある質問', 'よくある質問', 'manage_options', 'faq-options');

各引数の意味
SCF::add_options_page( $page_title, $menu_title, $capability, $menu_slug )

  1. $page_title
    管理画面の該当ページを開いたときに、ブラウザのタブなどに表示されるページタイトルです。
    ここでは「よくある質問」がタイトルとして表示されます。
  2. $menu_title
    WordPressの管理画面メニュー(左側メニュー)に表示される文言です。
    ここでは同じく「よくある質問」が表示されるメニュー項目になります。
  3. $capability
    このオプションページにアクセスできるユーザー権限(Capability)です。
    ここでは manage_options となっているため、通常は管理者(Administrator)のみがアクセス可能になります。
  4. $menu_slug
    ページ固有のスラッグ(識別子)です。
    ここでは 'faq-options' が指定されているので、管理画面URLの末尾に ?page=faq-options の形でアクセスできるようになります。

管理画面のメニューに作成したオプションページが追加されます

2. オプションフィールドの追加

  • フィールドグループの設定
    • 「SCF」→「フィールドグループ」→「新規追加」をクリック。
    • フィールドグループに名前を付けます(例:サイト全体の設定)。
  • フィールドを追加
    • 先ほど作成したフィールドグループ内で「フィールドを追加」をクリック。
    • 各フィールドの詳細を設定します。
    例:
    • フィールド名:site_logo
    • ラベル:サイトロゴ
    • フィールドタイプ:画像アップロード
  • 表示条件の設定
    • フィールドグループの表示条件を「オプションページ」に設定します。
    具体的には、「表示場所」→「オプションページ」を選択します。

3. オプションページへのデータ入力

  • データの入力
    • 「サイト設定」メニューに移動し、必要なデータ(例:サイトロゴ、連絡先情報など)を入力・保存します。

オプションページをテーマテンプレートで表示する方法

作成したオプションページのデータをテーマテンプレートで表示する手順は以下の通りです。

1. オプションフィールドの取得

  • PHPコードで取得
    • テーマテンプレート内で以下のようなコードを使用して、オプションフィールドのデータを取得・表示します。
<?php
if (function_exists('scf')) {
    // 例:サイトロゴの表示
    $site_logo = scf::get('site_logo', 'options'); // 'options'はオプションページを指定
    if ($site_logo) {
        echo '<img src="' . esc_url($site_logo) . '" alt="サイトロゴ">';
    }

    // 例:連絡先情報の表示
    $contact_email = scf::get('contact_email', 'options');
    if ($contact_email) {
        echo '<p>お問い合わせ: <a href="mailto:' . esc_attr($contact_email) . '">' . esc_html($contact_email) . '</a></p>';
    }
}
?>
Vercelアカウント作成、導入手順

Vercel が電話番号の入力を求めています

理由
セキュリティ強化のための追加認証
アカウントがセキュリティリスクとみなされている場合、電話番号を使用して認証コードを送信する手順を求められることがあります。

新しいデバイスやIPアドレスでのログイン
初めてのデバイスや新しいネットワークからログインする場合、Vercel が本人確認のために電話番号の入力を要求することがあります。

不正なアクセス防止
Vercel は、スパムや不正使用を防ぐために一部のアカウントで電話番号を要求することがあります。

対処法
電話番号を入力
信頼できる状況であれば、電話番号を入力し、認証コードを受け取ってログインを完了します。Vercel は入力した番号を認証以外の目的で使用しないと明記しています。

サポートに問い合わせる
電話番号を入力したくない場合、Vercel Support に問い合わせて代替の認証方法がないか確認することも可能です。

他のログイン方法を試す
別のGitプラットフォームアカウント(GitHub、GitLabなど)やメールアドレスを使用してログインを試すことも検討してください。

Vercel が要求するのは認証目的のみですので、セキュリティ面では問題ないとされています。ただし、不安な場合はサポートに相談するのが安心です。

【Vercel】v0とは?主要機能や使い方、料金体系を徹底解説!活用事例も紹介
https://www.ai-souken.com/article/what-is-v0-vercel

【Xserver】エックスサーバーにssh接続

フロー

SSH接続の基本フロー 1. コマンド実行: ssh [設定名] 2. ~/.ssh/config から設定を自動読み込み 3. 秘密鍵の自動読み込み 4. 公開鍵認証による自動接続

手順

フォルダ構成

~/.ssh/
│
├── config            # SSH接続設定ファイル(テキスト)
│                    # 複数サーバーの接続設定を記述可能
│
├── id_rsa           # 秘密鍵
│                    # 権限: 600(所有者のみ読み書き可)
│                    # 絶対に共有しない
│
├── id_rsa.pub       # 公開鍵
│                    # 権限: 644(所有者は読み書き可、他者は読み取りのみ)
│                    # サーバーに登録する
│
└── known_hosts      # 接続したことのあるサーバーの情報
                     # 初回接続時に自動生成

configファイルの例

# デフォルトの設定(すべてのHostに適用)
Host *
    # タイムアウトを設定(秒)
    ServerAliveInterval 60
    # 圧縮を有効化
    Compression yes

# 開発サーバーの設定例
Host dev
    # サーバーのホスト名またはIPアドレス
    HostName dev.example.com
    # SSHのポート番号(デフォルトは22)
    Port 22
    # ログインするユーザー名
    User developer
    # 使用する秘密鍵の場所
    IdentityFile ~/.ssh/id_rsa

# ステージングサーバーの設定例
Host stg
    HostName stg.example.com
    Port 22
    User deployer
    # 別の秘密鍵を使用する例
    IdentityFile ~/.ssh/staging_key

# 本番サーバーの設定例
Host prod
    HostName prod.example.com
    # セキュリティのため別ポートを使用
    Port 10022
    User production
    IdentityFile ~/.ssh/production_key
    # パスワード認証を無効化
    PasswordAuthentication no

~/.ssh/id_rsaの秘密鍵は以前作成したものを使いまわします

公開鍵をXserverの管理画面で登録

公開鍵を張り付ける

id_rsa.pub が公開鍵ファイルになります。このファイルの内容をXserverの管理画面にコピー&ペーストすることで、先ほどの秘密鍵(id_rsa)と対応する公開鍵認証が設定されます。

.ssh/configファイルを使ってssh接続できるようになります:

ssh xserver

SSHコマンドが設定ファイルの読み込みから認証まで、多くのことを自動的に処理してくれます。

スプレッドシートまとめ

ショートカットキー

「Ctrl + ?」でキーボードショートカットウィンドウを表示確認が可能です

別のシートの値を表示させたい

チェックボックスにチェックすると同じ行がグレーアウト

1)「表示形式」→「条件付き書式」

2)範囲に適用にグレーアウトするセルの範囲を入力

3)書式設定の条件のプルダウンから「カスタム数式」を選択

4)=$A3=trueと数式を入力

※複数チェックボックスにする場合 →=AND($A1=TRUE, $B1=TRUE, $C1=TRUE)

連番ID付与

B列に入力されるとA列に1から番号付与していきます

1)if(B4<>””,1,””)とA4に入力

if(条件,trueの式,falseの場合の式

2)if(B5<>””,A4₊1,””)とA5に入力

3)A5セルをドラッグして下まで同様に式を適用

ARRAYFORMULA()

スプレッドシートのARRAYFORMULA()は、複数のセルに対して一度に計算を行うための強力な機能です。

主な特徴:

  1. 一つの数式で複数のセルに結果を出力できる
  2. 配列や範囲全体を一度に処理できる
  3. 通常の数式よりも処理が高速

基本的な使い方:

  1. =ARRAYFORMULA()の括弧内に通常の数式を入力
  2. 結果は自動的に必要なセル数に展開される

例えば:

=ARRAYFORMULA(A1:A10 * 2)

これはA1からA10までの各セルの値を2倍にし、結果を10個のセルに出力します。

VLOOKUP

=VLOOKUP(検索値, 検索範囲, 列番号, [検索方法])
  • 検索値:探したい値を指定します。セル参照や直接入力した値を使用できます。
  • 検索範囲:検索を行うセル範囲を指定します。通常は複数の列を含む表全体を選択します。最初の列に検索値が含まれている必要があります。
  • 列番号:検索範囲内で、返したい値が含まれる列の番号を指定します。最初の列は1、2番目の列は2、というように数えます。
  • 検索方法:省略可能な引数です。TRUEまたはFALSEを指定します。
    • TRUE(または省略):近似一致。検索値以下の最大値を探します。
    • FALSE:完全一致。検索値と完全に一致するものだけを探します。

別シートからの参照:

同じスプレッドシート内の別シートを参照する場合、シート名を指定し、感嘆符(!)を使用して範囲を指定します。

=VLOOKUP(検索値, シート名!範囲, 列番号, [検索方法])

例:

Copy=VLOOKUP(A2, Sheet2!$A$1:$C$10, 2, FALSE)

この例では、現在のシートのA2セルの値を、”Sheet2″という名前のシートのA1:C10範囲で検索します。

別ファイルからの参照:

別のスプレッドシートファイルを参照する場合、IMPORTRANGE関数と組み合わせて使用します。

まず、IMPORTRANGE関数で別ファイルのデータを取り込みます:

=IMPORTRANGE("スプレッドシートのURL", "シート名!範囲")

そして、これをVLOOKUP関数の検索範囲として使用します:

=VLOOKUP(検索値, IMPORTRANGE("スプレッドシートのURL", "シート名!範囲"), 列番号, [検索方法])

例:

=VLOOKUP(A2, IMPORTRANGE("https://docs.google.com/spreadsheets/d/abcdefghijklmnop", "Sheet1!A1:C10"), 2, FALSE)

この例では、別のスプレッドシートファイルのSheet1のA1:C10範囲をインポートし、その中でA2セルの値を検索します。

注意点:

  • 別ファイルを参照する場合、最初にIMPORTRANGE関数へのアクセス許可を与える必要があります。
  • ファイルへのアクセス権限が必要です。
  • 大量のデータを参照する場合、パフォーマンスに影響する可能性があります。

列を増やす

初期状態のシートは26列(Z列まで)しかありません

右端の列で増やしたい数だけ行をコピーし、右クリックして「特殊貼り付け」→「転置して貼り付け」

【生成AI】「まだうまく言語化できない」状態でも活用できる質問の仕方や、プロンプト言い回しの例

プロンプト例

  • ひとつずつスモールステップでお願いします(有効な手段から)1ステップずつ回答してください、その都度こちらから必要に応じて質問します
  • 常に難しい用語はその説明も併せてお願いします
  • 指示をする根拠をおしえてください
  • 必要があればSVG等で視覚的にお願いします
- 修正したコードでどんな理由で何をしているかのコメントアウトで詳しく説明を加えてください
- 下記の通り修正履歴もコメントアウトで残してください

/**
* 修正履歴コメントのサンプル
* 
* 文字化け解消のための修正履歴:
* 1. Content-Typeヘッダーの設定を試行
*    res.setHeader('Content-Type', 'application/json; charset=utf-8');
*    
* 2. Buffer経由のデコードを試行
*    Buffer.from(rows[0].content).toString('utf8')
* 
* 3. クエリでのCONVERTを試行
*    SELECT CONVERT(content USING utf8mb4) as content
*    SELECT CONVERT(CAST(content AS BINARY) USING utf8) as content
* 
* 4. 現在の修正案:セッション文字コードの明示的な設定と
*    バイナリ経由での文字コード変換
*/
  • コメントアウトに記述してある通り色々アプローチしましたが改善されません、ほかにより包括的なアプローチ提案できますか?

プロジェクトの構成ファイルやディレクトリを確認する場合

Get-ChildItem -Path . -Depth <数字> -Force

# サブディレクトリのファイルも表示する場合
tree /f

# 特定のファイルタイプを除外する場合(例:.gitを除外)
tree /f /a | findstr /v ".git"

「まだうまく言語化できない」状態でも活用できる質問の仕方

まずは背景を伝える

質問の要点を整理しきれていないときでも、まずは自分が今どんな作業・状況にいるかを共有するだけで、相手がヒントを出しやすくなります。

たとえば「○○の機能を実装中なんですが、動かしてみるとエラーが出ます。エラー内容を一通り検索してみたんですが、似たような事例は見当たりませんでした」といった形で簡単に状況を説明しましょう。

分かっていることと分からないことを区別する

まだうまく言語化できていない段階でも、「分かっている点」「分からない点」をざっくりと区別できるよう意識しましょう。

「原因は不明だけど、エラーが出るタイミングは分かっている」「作業手順は知っているが、どこに注目すればいいかピンとこない」など、断片的でも構いません。 これだけで相手が「ここから話を聞けばよさそうだな」と思いやすくなります。

「うまく言語化できない」状態を素直に伝える

質問者である自分自身が混乱しているときは、「どこが分からないのかよく分からなくなってきました」という事実をそのまま伝えてしまうのも有効です。 相手は慣れている可能性が高いので、「では一度、○○から整理してみましょうか」とステップ・バイ・ステップで導いてくれるでしょう。

具体的な言い回し例

言い回し例ポイント
「○○の機能を触っているのですが、どうにもエラーが出て原因が分からないです。エラー文は△△で、検索してもヒントがなくて…まず何を確認したらよいでしょうか?」具体的に「エラー文」や「すでに調べた内容」を伝えると、相手が“次の調査ポイント”を提案しやすくなる
「実は、どこが分からないのか自分でもはっきりしないんです。おそらく□□あたりが怪しいと思うのですが、何が論点なのか一緒に整理していただけると助かります」「整理してほしい」「いっしょに考えてほしい」というリクエストを明確にすることで、相手は段階を踏んだ説明をしやすくなる
「最終的に××機能を完成させたいのですが、どういう手順や知識が必要かイメージできていません。まずはどのあたりから手をつければいいでしょうか?」ゴールを示しつつ、段階的な手引きを求める言い方。相手の経験を生かしたアドバイスを得やすい
「あいまいな部分が多くてすみません。もし▲▲や■■について先に理解しておいたほうがいい場合は教えてください。自分でもあわせて勉強します」事前知識の不足を自覚していることを伝えると、相手は必要なキーワードや前提知識を提案しやすくなる

台本サンプル

ミーティングの冒頭・あいさつ

  • 本日のビデオチャットは私の方で進行いたします。よろしくお願いいたします。
  • お疲れさまです。今日はいつもどおり、前回までの進捗と、現在抱えている疑問点を整理して相談させてください。
  • スケジュールは○○時までを予定しておりますが、問題ないでしょうか

取り上げたいトピックの概要

  • ssh接続で別のPCでDockerコマンドで操作、Linuxコマンド
    →現在の進捗作業をお話しするので、方針として問題ないか、その他アドバイス等あればお伺いしたいです。
  • APIの種類、オープン、ウェブ…
    →○○に関するドキュメントとネット検索でざっと目を通したが、よく理解できていない。恐縮ですが、実装経験をお持ちかなと思いまして、可能であれば少し知見を伺いたいのですが
  • WordPress開発環境構築、テーマフォルダはGithub、それ以外のコンテンツはDBから
    →現状○○という問題発生しており、改善案や優先して調査すべきポイントがあれば…
  • 【インフラ】プロキシサーバーで制限がかかっている内容を把握、ifilter、管理画面のログやリストは見れない状況
    お聞きしたい質問内容は…経緯として…
    今はまだ“どこが問題の本質なのか”を自分でもちゃんと整理しきれていません。
    もし私の説明があいまいで分かりにくいようでしたら、追加で質問していただけると助かります。
    一緒に整理していただけるとありがたいです。

クロージング

MySQLでの文字化けトラブル解決記録(Docker + Express + MySQL で作る AI コードフォーマッター)

MySQLでの文字化けの発生

当初、VBA Formatterプロジェクトで日本語を含むプロンプトデータを表示した際に文字化けが発生していました。
具体的には「あいうえお」が「縺ゅ>縺」のように表示される状態でした。

文字コードとは

文字コードの基本

文字コードとは、コンピュータが文字を扱うための約束事です。
コンピュータは内部的には全て数値(バイナリ)で処理するため、
「あ」という文字を「あ」として認識するためには、決まった規則が必要になります。

主な文字コード

  • ASCII: 英数字のみ(128文字)
  • Shift-JIS: 日本語用(Windows系)
  • EUC-JP: 日本語用(UNIX系)
  • UTF-8: 世界中の文字に対応(現在の標準)
  • UTF-8mb4: UTF-8の拡張版(絵文字にも対応)

なぜ文字化けが起きたのか

文字化けの主な原因は、データの流れる過程で文字コードの解釈が一貫していなかったことです:

  1. データベースでの保存時の文字コード
  2. アプリケーションでの処理時の文字コード
  3. ブラウザでの表示時の文字コード

これらの設定が異なると、例えば:

  • UTF-8で保存したデータを
  • Shift-JISとして読み込み
  • UTF-8として表示しようとする

というような不整合が発生し、文字化けの原因となります。

解決方法

データベース層での対応

Node.js(Express)とMySQLをDockerで構築する手順を、順を追って解説します。

-- データベースの文字コード設定
SET NAMES utf8mb4;
SET CHARACTER SET utf8mb4;

-- テーブル作成時の設定
CREATE TABLE prompts (
    -- カラム定義
) CHARACTER SET utf8mb4 
  COLLATE utf8mb4_unicode_ci;

アプリケーション層での対応

// データベース接続設定
const config = {
    charset: 'utf8mb4',
    collation: 'utf8mb4_unicode_ci'
};

// クエリ実行時の設定
await db.query("SET NAMES utf8mb4");

フロントエンド層での対応

// バイナリデータとして受け取り、適切にデコード
const buffer = await response.arrayBuffer();
const decoder = new TextDecoder('utf-8');
const jsonString = decoder.decode(buffer);

プロジェクト構成

project/
├── docker-compose.yml
├── Dockerfile
├── package.json
├── .env
└── src/
    ├── index.js
    ├── bedrock/
    │   ├── analyzer.js
    │   └── client.js
    ├── db/
    │   ├── connection.js
    │   └── init/
    │       └── 01-schema.sql
    └── public/
        ├── index.html
        ├── style.css
        └── script.js

主要ファイルの実装

Dockerコンテナ作成

docker-compose.yml

version: '3.8'
services:
  app:
    build: .
    ports:
      - "4000:4000"
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    environment:
      - PORT=4000
      - DB_HOST=db
      - DB_USER=vbauser
      - DB_PASSWORD=vbapassword
      - DB_NAME=vba_formatter
    depends_on:
      - db

  db:
    image: mysql:8.0
    platform: linux/amd64

    # 文字コードの設定:日本語を正しく扱うためのMySQLサーバー設定
    command: 
        - --character-set-server=utf8mb4
        - --collation-server=utf8mb4_unicode_ci
    ports:
      - "3307:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: vba_formatter
      MYSQL_USER: vbauser
      MYSQL_PASSWORD: vbapassword
      TZ: Asia/Tokyo
    volumes:
      - mysql_data:/var/lib/mysql
      - ./src/db/init:/docker-entrypoint-initdb.d

volumes:
  mysql_data:

Dockerfile

FROM node:20-slim

WORKDIR /usr/src/app

# ロケールのインストールと設定 (Debianベース)
RUN apt-get update && apt-get install -y locales && \
    sed -i 's/# ja_JP.UTF-8/ja_JP.UTF-8/' /etc/locale.gen && \
    locale-gen ja_JP.UTF-8 && \
    apt-get clean

ENV LANG=ja_JP.UTF-8
ENV LC_ALL=ja_JP.UTF-8
ENV LANGUAGE=ja_JP:ja

# MySQLクライアントパッケージの名前を修正
RUN apt-get update && apt-get install -y default-mysql-client

# アプリケーションの依存関係をコピー
COPY package*.json ./

# 依存関係のインストール
RUN npm install

# アプリケーションのソースをコピー
COPY . .

EXPOSE 4000

CMD ["npm", "run", "dev"]

package.json

{
  "name": "vba-formatter",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "@aws-sdk/client-bedrock-runtime": "^3.0.0",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "iconv-lite": "^0.6.3",
    "mysql2": "^3.6.5",
    "nodemon": "^3.0.2"
  }
}

Express サーバーの実装

データベース接続設定 (src/db/connection.js)

// src/db/connection.js

/**
 * MySQL2のPromise版を使用
 * Promise-basedなAPIでデータベース操作を行う
 */
const mysql = require('mysql2/promise');

/**
 * データベース接続の修正履歴 
 * 
 * 1. 接続エラーの解消
 *    - 接続タイムアウトの設定追加
 *    - リトライロジックの実装
 *    - エラーハンドリングの強化
 * 
 * 2. コネクションプールの最適化
 *    - プール設定の調整
 *    - 接続数の制限設定
 * 
 * 3. デバッグ機能の強化
 *    - 詳細なログ出力の追加
 *    - 接続状態の監視機能
 */

/**
 * データベース接続の文字コード処理の詳細説明
 * 
 * 【文字化けが発生理由】
 * データベースとアプリケーション間でデータをやり取りする際、
 * 以下の3つのポイントで文字コードの変換が発生します:
 * 
 * 1. アプリケーション → データベース(データ送信時)
 * 2. データベース内でのデータ保存
 * 3. データベース → アプリケーション(データ取得時)
 * 
 * 【設定項目の説明】
 * 1. charset: 'utf8mb4'
 *    - 接続時の文字コードを指定
 *    - データベースとの通信で使用する文字コードを決定
 * 
 * 2. collation: 'utf8mb4_unicode_ci'
 *    - 文字の照合順序を指定
 *    - 「ci」は Case Insensitive(大文字小文字を区別しない)
 * 
 * 3. initializationCommands
 *    - 接続確立直後に実行されるコマンド
 *    - セッションごとに文字コード設定を確実に行う
 * 
 * 【改善の仕組み】
 * - 接続時に文字コード設定を強制的に行う
 * - セッションごとに設定を初期化
 * - バイナリデータ経由で確実な文字コード変換を実現
 */

/**
 * データベース接続設定の拡張
 * - 環境変数から設定を読み込み、なければデフォルト値を使用
 * - utf8mb4を使用して絵文字を含む多言語対応
 */
const config = {
    host: process.env.DB_HOST || 'db',
    user: process.env.DB_USER || 'vbauser',
    password: process.env.DB_PASSWORD || 'vbapassword',
    database: process.env.DB_NAME || 'vba_formatter',
    charset: 'utf8mb4',
    collation: 'utf8mb4_unicode_ci',
    // 文字コード関連の設定を追加
    connectionLimit: 10,
    supportBigNumbers: true,
    bigNumberStrings: true,
    dateStrings: true,
    // 明示的な文字コード設定
    charset: 'utf8mb4',
    // コネクション確立時の初期化コマンド
    initializationCommands: [
        'SET NAMES utf8mb4',
        'SET CHARACTER SET utf8mb4',
        'SET SESSION collation_connection = utf8mb4_unicode_ci'
    ]
};

// コネクションプールのインスタンス
let pool;
let connectionAttempts = 0;
const MAX_RETRIES = 5;

/**
 * データベース接続を試行する関数
 * リトライロジック付き
 */
const initPool = async () => {
    while (connectionAttempts < MAX_RETRIES) {
        try {
            console.log(`データベース接続を試行中... (試行回数: ${connectionAttempts + 1})`);
            
            // プールの作成
            pool = mysql.createPool(config);
            
            // 接続テスト
            await pool.query('SELECT 1');
            
            // 文字コード設定の確認
            const [charsetResults] = await pool.query('SHOW VARIABLES LIKE "character%"');
            console.log('データベース文字コード設定:', charsetResults);
            
            console.log('データベース接続成功');
            return pool;

        } catch (error) {
            connectionAttempts++;
            console.error(`データベース接続エラー (試行回数: ${connectionAttempts}):`, error);
            
            if (connectionAttempts >= MAX_RETRIES) {
                throw new Error(`データベース接続に失敗しました。最大試行回数(${MAX_RETRIES})を超えました。`);
            }
            
            // 再試行前に待機
            await new Promise(resolve => setTimeout(resolve, 2000 * connectionAttempts));
        }
    }
};

/**
 * クエリ実行用の関数
 * 接続エラー時の再接続を含む
 */
const query = async (...args) => {
    try {
        if (!pool) {
            await initPool();
        }
        return await pool.query(...args);
    } catch (error) {
        console.error('クエリ実行エラー:', error);
        // 接続エラーの場合は再接続を試みる
        if (error.code === 'PROTOCOL_CONNECTION_LOST') {
            pool = null;
            return query(...args);
        }
        throw error;
    }
};

/**
 * データベース操作用の関数をエクスポート
 * - query: SQL実行用の関数
 * - getPool: プールインスタンス取得用の関数
 */
module.exports = {
    // クエリ実行関数
    query,
    // プール取得関数
    getPool: async () => {
        if (!pool) {
            await initPool();  // プールが未初期化なら初期化
        }
        return pool;
    }
};

src\db\init\01-schema.sql

/**
 * 文字コード設定の詳細説明
 * 
 * 【文字化け発生の背景】
 * データベースでは文字データを保存する際に「文字コード」という形式を使用します。
 * 日本語などの多言語文字を正しく扱うにはUTF-8mb4という文字コードが必要です。
 * 
 * 【各設定の役割】
 * 1. SET NAMES utf8mb4
 *    - クライアントとサーバー間の通信で使用する文字コードを設定
 *    - データの送受信時の文字化けを防ぐ
 * 
 * 2. SET CHARACTER SET utf8mb4
 *    - データベースが使用する文字コードを設定
 *    - データ保存時の文字化けを防ぐ
 * 
 * 3. COLLATE utf8mb4_unicode_ci
 *    - 文字の照合順序(ソート順)を設定
 *    - 日本語を含む多言語での正しい並び順を保証
 * 
 * 【utf8mb4を使用する理由】
 * - 絵文字を含むすべてのUnicode文字を扱える
 * - 従来のutf8より広い文字範囲をサポート
 * - 将来的な文字コードの拡張にも対応可能
 */

-- データベースの文字コード設定を確実に行う
SET NAMES utf8mb4;
SET CHARACTER SET utf8mb4;

-- データベースの文字コードを強制的にUTF8mb4に設定
-- これによりデータベースレベルで日本語を正しく扱える
ALTER DATABASE vba_formatter CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- カラム
CREATE TABLE IF NOT EXISTS prompts (
    id VARCHAR(50) PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB 
  CHARACTER SET utf8mb4 
  COLLATE utf8mb4_unicode_ci;

/**
 * プロンプトの表示順序の制御について
 * 
 * 【表示順序の制御方法】
 * 1. nameカラムに接頭辞を付けて制御
 *    - 数字の場合はゼロ埋めして2桁で統一
 *    - 例:'01_', '02_' など
 * 
 * 2. 意図した順序:
 *    - "01_VBAプロンプト1" (最初に表示)
 *    - "02_VBAプロンプト2" (2番目に表示)
 *    - "03_HTML BEMの命名規則" (3番目に表示)
 *    - "04_開発テスト用" (4番目に表示)
 */

-- 初期データの挿入(順序を制御するため、nameを修正)
INSERT INTO prompts (id, name, content) VALUES 
    ('prompt01', '01_VBAプロンプト1', 
    '# ここからがAIへの指示内容です
=============================================

# AIの役割定義
あなたはVBAコードの専門家として、以下のコードを分析し、優先順位に従って改善してください。

# 入力コード部分
```vba
{code}
```

# 改善要件(優先順位:高 - 第一弾)
=============================================
1. インデント処理
  - SUBからENDSUBまでTABひとつ分のインデント
  - IF文、With文、For文等の制御構造にもインデント
  - ネストレベルに応じて適切なインデント

2. ヘッダー・フッター追加
  - Sub開始時のヘッダー:
    Option Explicit
    ''***************************************************
    '' [プロシージャ名]
    ''***************************************************
    ''【機  能】[機能の説明]
    ''【引  数】[引数の説明]
    ''【戻 り 値】[戻り値の説明]
    ''【機能説明】[詳細な説明]
    ''【備  考】[その他特記事項]
    '' Copyright(c) 2024 Your Company All Rights Reserved.
    ''***************************************************
    
  - 処理の区切り:
    ''***************************************************
    '' 変数宣言
    ''***************************************************
    [変数宣言部分]

  - Sub終了時のフッター:
    ''***************************************************
    ''---------------------------------------------------
    '' Version
    ''---------------------------------------------------
    '' 1.00 | 2024.01.01 | ********* | *********
    ''***************************************************

# 改善要件(優先順位:中 - 第二弾)
=============================================
3. 変数名の改善
  - 一文字の変数を禁止
  - セル参照の変数は意味のある名前に変更
    (例: Last_Row, Last_Column)
  - 配列はArrayを付ける
  - その他の変数はDataを付ける

4. Public変数・Call文の処理
  - Public変数にはPublic関数であることをコメントで記載
  - Call文にはモジュール名を追加
    (例: Call Module1.印刷)

5. 変数宣言のコメント
  - 宣言した変数の横にコメントを追加
  - 使用目的や内容を簡潔に説明

# 改善要件(優先順位:中 - 第三弾)
=============================================
6. 変数・シート名の処理
  - 日本語変数名を英語に変更
  - 未宣言変数を「変数宣言」セクションに追加
  - 型判定できない変数はVariantに
  - シート名は定数(Const)で定義
  - シート追加時の名前はVariantで処理

7. コメント追加
  - IF文、With文などの制御構造にコメント
  - 配列の内容説明
  - SET文の説明
  - Offsetのコメント必須
    (参照セルと目的を明記)

8. コードの最適化
  - 類似コードが3回以上続く場合はループ化
  - パスの直書きは避け、pathに置換
    (元のパスはコメントとして保持)

# 出力形式
=============================================
優先順位の高い要件(第一弾)から順に適用し、
改善したVBAコードのみを出力してください。'),

    ('prompt02', '02_VBAプロンプト2',
    '# ここからがAIへの指示内容です(コードレビュー版)
=============================================

# AIの役割定義
あなたはシニアVBA開発者として、以下のコードをレビューし、ベストプラクティスに基づいて改善してください。

# 入力コード部分
```vba
{code}
```

# レビュー観点
=============================================
1. コーディング規約準拠
2. バグの可能性
3. パフォーマンスボトルネック
4. セキュリティリスク

# 出力形式
=============================================
コードレビューコメントと改善後のコードを提供してください。'),

    ('prompt03', '03_HTML BEMの命名規則',
    '# ここからがAIへの指示内容です(HTML BEM分析版)
=============================================

# AIの役割定義
あなたはHTMLとCSSの専門家として、クラス名をBEM命名規則に基づいて改善します。

# 入力コード部分
```html
{code}
```

# 改善要件
=============================================
1. BEMの基本ルール適用
  - Block: 独立したコンポーネント(例: header, menu)
  - Element: Blockの一部(例: menu__item)
  - Modifier: 状態や見た目の変更(例: menu__item--active)

2. 命名規則の統一
  - Blockは意味のある名前を使用
  - Element は __ (アンダースコア2つ) で接続
  - Modifier は -- (ハイフン2つ) で接続
  - 全て小文字、ハイフンで単語を区切る

3. クラス名の階層構造
  - 最大2階層までの要素の入れ子
  - Block内のElement同士の依存関係を避ける
  - 共通の機能はMixinとして抽出

4. コンポーネントの分割
  - 再利用可能なBlockの特定
  - 共通のModifierパターンの抽出
  - コンポーネント間の依存関係の最小化

5. レスポンシブ対応
  - Modifierでのブレイクポイント管理
  - コンテナクエリの活用
  - フレックスボックス/グリッドの適切な使用

# 出力形式
=============================================
1. 改善後のHTML
2. 各クラス名の説明とBEMルールとの対応
3. コンポーネント構造の解説'),

    ('prompt04', '04_開発テスト用',
    '# ここからがAIへの指示内容です
=============================================

# 以下のコードをレビューし、改善要件に基づいて修正してください。
-
# 入力コード部分
```
{code}
```
# 改善要件
=============================================
1. xx
  - xx
  - xx
  - xx

2. xx
  - xx
  - xx
  - xx

3. xx
  - xx
  - xx
  - xx

# 出力形式
=============================================
1. xx
2. xx
3. xx')
ON DUPLICATE KEY UPDATE 
    name = VALUES(name),
    content = VALUES(content);

メインサーバーファイル (src/index.js)

// ========================================
// サーバーのメインファイル (index.js)
// VBAフォーマッターのバックエンド処理を担当
// ========================================

// 必要なモジュールのインポート
const path = require("path"); // pathモジュールを追加
const express = require("express");
const {analyzeCode} = require("./bedrock/analyzer"); // Bedrock AIの分析機能
// const { formatVBA } = require('./formatter'); // 基本的なフォーマット、現在機能使用していない
const db = require("./db/connection"); // データベース接続を追加

// Expressアプリケーションの初期化
const app = express();

// ミドルウェアの設定
// 静的ファイルのパスを修正- Docker環境での絶対パス指定に変更
app.use(express.static(path.join(__dirname, "public")));
// JSONリクエストの解析を有効化
app.use(express.json());

// CORSの設定を追加
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");
  res.header("Access-Control-Allow-Headers", "Content-Type");
  next();
});

// Content-Typeの設定(APIエンドポイントのみに適用)
app.use((req, res, next) => {
  if (
    req.path.startsWith("/api") ||
    req.path.startsWith("/prompts") ||
    req.path === "/format" ||
    req.path === "/prompt-list"
  ) {
    res.setHeader("Content-Type", "application/json; charset=utf-8");
    // 追加: レスポンスエンコーディングを設定
    res.setHeader("Transfer-Encoding", "chunked");
  }
  next();
});

/**
 * プロンプト一覧取得APIをDB対応に修正
 * - MySQLからデータを取得するように変更
 */
app.get("/prompt-list", async (req, res) => {
  try {
    const [rows] = await db.query("SELECT id, name, description FROM prompts");
    res.json({
      success: true,
      prompts: rows,
    });
  } catch (error) {
    console.error("データベースエラー:", error);
    res.status(500).json({
      success: false,
      error: "プロンプト一覧の取得に失敗しました",
    });
  }
});

app.get("/about", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "about.html"));
});

/**
 * 文字化け解消のための修正履歴 2024-03-xx
 * 
 * 1. Content-Typeヘッダーの設定を試行
 *    res.setHeader('Content-Type', 'application/json; charset=utf-8');
 *    → 部分的な効果あり、完全な解決には至らず
 *    
 * 2. Buffer経由のデコードを試行
 *    Buffer.from(rows[0].content).toString('utf8')
 *    → 一部の文字で文字化けが発生
 * 
 * 3. クエリでのCONVERT使用を試行
 *    SELECT CONVERT(content USING utf8mb4) as content
 *    SELECT CONVERT(CAST(content AS BINARY) USING utf8) as content
 *    → 特定の文字で文字化けが継続
 * 
 * 4. HEX形式での取得を試行
 *    SELECT HEX(content) as content_hex FROM prompts
 *    → デコード時に問題発生
 * 
 * 5. クエリオプションでのエンコーディング指定を試行
 *    {sql: "SELECT * FROM prompts", encoding: 'utf8mb4'}
 *    → mysql2では未サポート
 * 
 * 6. セッション文字コードの設定とバイナリ変換を試行
 *    SET NAMES utf8mb4 +
 *    SELECT CAST(CONVERT(content USING binary) AS CHAR CHARACTER SET utf8mb4)
 *    → 部分的な改善
 * 
 * 7. iconv-liteによる文字コード変換を試行
 *    const iconv = require('iconv-lite');
 *    iconv.decode(Buffer.from(content), 'utf8');
 *    → 特定のケースで例外発生
 * 
 * 8. execute()による実行を試行
 *    → db.executeは存在しない
 *    → プールインスタンスにはexecuteメソッドがない
 * 
 * 9. コネクション管理の改善を試行
 *    - getConnection()でコネクションを取得
 *    - 取得したコネクションでexecuteを実行
 *    - 処理後にコネクションを解放
 *    → コネクション管理は改善したが文字化けは解消せず
 * 
 * 10. 現在の解決策(2024-03-xx):
 *     - データベースレベルでの文字コード設定の確実な適用
 *     - 接続時の初期化コマンドによる文字コード設定
 *     - バイナリ経由での確実な文字コード変換
 *     - レスポンスヘッダーとエンコーディングの最適化
 *     → 以下のアプローチの組み合わせで解決
 *        1. データベース接続時の文字コード初期化
 *        2. バイナリ経由での文字コード変換
 *        3. Buffer処理による確実なエンコーディング
 *        4. クライアントでのデコード処理の最適化
 */

/**
 * APIエンドポイントでの文字コード処理の詳細説明
 * 
 * 【文字化け解消の3段階】
 * 1. データベースからの取得時
 *    - バイナリデータとして一旦取得
 *    - UTF-8mb4として再解釈
 * 
 * 2. JSONレスポンス生成時
 *    - Buffer経由で文字コードを確実に変換
 *    - 文字化けしやすい文字も正しく処理
 * 
 * 3. クライアントへの送信時
 *    - Content-Typeヘッダーで文字コードを明示
 *    - Transfer-Encodingの設定で大きなデータも安全に送信
 * 
 * 【なぜこの方法で解決できたのか】
 * 1. バイナリ経由の変換
 *    - 文字コードの解釈を一旦リセット
 *    - 確実にUTF-8として再解釈
 * 
 * 2. 多層的なアプローチ
 *    - DB設定
 *    - 接続設定
 *    - アプリケーション処理
 *    全ての層で文字コードを適切に処理
 * 
 * 3. ヘッダー設定の最適化
 *    - クライアントに文字コードを正しく伝達
 *    - ブラウザでの解釈を確実に制御
 */

// プロンプト一覧を取得するエンドポイント
app.get("/api/prompt-info", async (req, res) => {
  try {
    // 文字コードの明示的な設定
    await db.query("SET NAMES utf8mb4");
    await db.query("SET CHARACTER SET utf8mb4");
    await db.query("SET SESSION collation_connection = utf8mb4_unicode_ci");
    
    // バイナリ経由での文字列取得(最新の解決策)
    const [rows] = await db.query(`
      SELECT 
        id,
        CONVERT(CAST(CONVERT(name USING binary) AS BINARY) USING utf8mb4) as name,
        CONVERT(CAST(CONVERT(content USING binary) AS BINARY) USING utf8mb4) as content
      FROM prompts 
      ORDER BY name
    `);
    
    // レスポンスヘッダーの設定
    res.setHeader('Content-Type', 'application/json; charset=utf8');
    res.setHeader('Transfer-Encoding', 'chunked');
    
    // Buffer経由でのエンコーディング
    const jsonString = JSON.stringify({
      success: true,
      prompts: rows.map(row => ({
        id: row.id,
        name: Buffer.from(row.name).toString('utf8'),
        content: Buffer.from(row.content).toString('utf8')
      }))
    });
    
    res.end(Buffer.from(jsonString, 'utf8'));

  } catch (error) {
    console.error("データ取得エラー:", error);
    res.status(500).json({
      success: false,
      error: "データの取得に失敗しました"
    });
  }
});

// 個別のプロンプト内容を取得するエンドポイント
app.get("/api/prompts/:id", async (req, res) => {
  // レスポンスヘッダーを設定
  res.setHeader('Content-Type', 'application/json; charset=utf-8');

  try {
    // UTF-8エンコーディングを明示的に指定してコンテンツを取得
    const [rows] = await db.query(
      "SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) as content FROM prompts WHERE id = ?",
      [req.params.id]
    );

    if (!rows || rows.length === 0) {
      return res.status(404).json({
        success: false,
        error: "指定されたプロンプトが見つかりません"
      });
    }

    // 成功時のレスポンス形式を統一
    res.json({
      success: true,
      content: rows[0].content
    });

  } catch (error) {
    // エラーログの出力
    console.error("プロンプト取得エラー:", error);
    
    // エラー時のレスポンス形式を統一
    res.status(500).json({
      success: false,
      error: "プロンプトの取得に失敗しました",
      details: process.env.NODE_ENV === 'development' ? error.message : undefined
    });
  }
});

/**
 * フォーマットAPIにプロンプトIDの処理を追加
 * - リクエストからプロンプトIDを取得
 * - DBから対応するプロンプトを取得して使用
 */
app.post("/format", async (req, res) => {
  try {
    const {code, promptId} = req.body;
    const [rows] = await db.query(
      "SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) as content FROM prompts WHERE id = ?",
      [promptId]
    );

    if (!rows || rows.length === 0) {
      return res.status(404).json({
        success: false,
        error: "プロンプトが見つかりません",
      });
    }

    const formattedCode = await analyzeCode(code, rows[0].content);
    res.json({success: true, formatted: formattedCode});
  } catch (error) {
    console.error("Format error:", error);
    res.status(500).json({
      success: false,
      error: error.message || "フォーマット処理に失敗しました",
    });
  }
});

// プロンプト更新API
app.put("/prompts/:id", async (req, res) => {
  console.log("Update request received:", {
    params: req.params,
    body: req.body,
  });

  try {
    const {id} = req.params;
    const {content} = req.body;

    // データベース接続テスト
    const [testConnection] = await db.query("SELECT 1");
    console.log("Database connection test:", testConnection);

    const [result] = await db.query(
      "UPDATE prompts SET content = ? WHERE id = ?",
      [content, id]
    );

    console.log("Update result:", result);

    if (result.affectedRows === 0) {
      return res.status(404).json({
        success: false,
        error: "プロンプトが見つかりません",
      });
    }

    res.json({
      success: true,
      message: "更新しました",
    });
  } catch (error) {
    console.error("Database error:", error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// サーバー起動
const PORT = process.env.PORT || 4000;
app.listen(PORT, "0.0.0.0", () => {
  console.log("========================================");
  console.log(`Server running on http://localhost:${PORT}`);

  // 登録されているルートを表示
  app._router.stack.forEach((r) => {
    if (r.route && r.route.path) {
      console.log(`Route: ${r.route.path}`);
      console.log(`Methods:`, r.route.methods);
    }
  });
  console.log("========================================");
});

bedrock関連

.envファイルで要環境変数設定

src\bedrock\analyzer.js

// AWS SDKからBedrockのクライアントとコマンドをインポート
const { InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime");
const client = require('./client');

/**
 * コードをClaudeモデルで分析する関数
 * @param {string} code - 分析対象のコード(VBAまたはHTML)
 * @param {string} promptContent - 使用するプロンプトの内容
 * @returns {Promise<string>} - 改善されたコード
 */
async function analyzeCode(code, promptContent) {
    try {
        // 入力検証
        if (!code || !promptContent) {
            throw new Error('コードとプロンプトの内容は必須です');
        }

        // プロンプト内容にコードを挿入
        // {code}をユーザー入力のコードに置換
        const fullPrompt = promptContent.replace('{code}', code);

        // Claude用のリクエストコマンドを作成
        const command = new InvokeModelCommand({
            // Claude 3 Sonnetモデルを指定(最新バージョン)
            modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
            contentType: "application/json",
            accept: "application/json",
            // Bedrock API用のリクエストボディを構築
            body: JSON.stringify({
                // Bedrockのバージョンを指定
                anthropic_version: "bedrock-2023-05-31",
                // 最大トークン数(出力の長さ制限)
                max_tokens: 2048,
                // メッセージ形式でプロンプトを送信
                messages: [
                    {
                        role: "user",
                        content: fullPrompt
                    }
                ],
                // 必要に応じて追加のパラメータを設定可能
                // temperature: 0.7,  // 生成の多様性(0-1)
                // top_p: 0.9,       // 出力のランダム性
            })
        });

        // BedrockAPIを呼び出し
        const response = await client.send(command);
        
        // レスポンスの処理(バイナリからUTF-8文字列に変換)
        const responseBody = Buffer.from(response.body).toString('utf8');
        const jsonResponse = JSON.parse(responseBody);

        // Claudeのレスポンス形式から結果を抽出
        // content[0].textに実際の生成テキストが含まれる
        if (jsonResponse.content && jsonResponse.content[0]) {
            return jsonResponse.content[0].text;
        }

        throw new Error('AIモデルからの応答が不正な形式です');

    } catch (error) {
        // エラーログを出力し、上位層に伝播
        console.error("コード分析エラー:", error);
        console.error("エラー詳細:", {
            message: error.message,
            code: error.code,
            requestId: error.$metadata?.requestId
        });
        throw error;
    }
}

// analyzeCode関数をエクスポート
module.exports = {
    analyzeCode
};

src\bedrock\client.js

require('dotenv').config();
const { BedrockRuntimeClient } = require("@aws-sdk/client-bedrock-runtime");

// Bedrockクライアントの設定
const client = new BedrockRuntimeClient({ 
    region: process.env.AWS_REGION || "us-east-1",
    credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
    }
});

module.exports = client;

フロントエンド

src/public/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <!-- 文字エンコーディングとビューポートの設定 -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>VBA Formatter</title>
    <!-- reset.css ress -->
    <link rel="stylesheet" href="https://unpkg.com/ress/dist/ress.min.css" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link rel="stylesheet" href="style.css">
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">

</head>
<body>
    <!-- メインコンテナ -->
    <div class="container">
        <h1>VBA Formatter</h1>
        <div class="nav-links">
            <a href="index.html">ホーム</a>
            <a href="about.html">About</a>
        </div>

        <!-- AIへの指示内容表示セクション(src/bedrock/prompts.jsの内容) -->
        <div class="prompt-section">
            <h2>AIへの指示内容</h2>
            <div class="prompt-selector">
                <select id="promptSelect" onchange="changePrompt()">
                    <option value="">読み込み中...</option>
                </select>
                <button id="editButton" onclick="toggleEdit()" class="edit-button">
                    編集
                </button>
            </div>
            <!-- プロンプト内容を動的に表示する要素 -->
            <div class="prompt-display-container">
                <div class="user-icon"></div>
                <div class="prompt-display-wrapper">
                    <pre id="promptDisplay" class="prompt-display" contenteditable="false">ユーザーからの指示内容がここに表示されます</pre>
                </div>
            </div>
            <button id="saveButton" onclick="savePrompt()" class="save-button" style="display: none;">
                保存
            </button>
        </div>

        <!-- コードエディタ部分 -->
        <div class="editors">
            <!-- 入力用エディタ -->
            <div class="editor">
                <h2>分析するコード</h2>
                <!-- 入力用テキストエリア:ユーザーがコードを入力または貼り付け可能 -->
                <textarea id="input" placeholder="コードを入力してください"></textarea>
                
                <!-- ファイル選択ボタン:.basと.txtファイルのみ受付 -->
                <input type="file" id="vbaFile" accept=".bas,.txt,.html,.css,.js">
            </div>
            <!-- 出力用エディタ -->
            <div class="editor">
                <h2>改善後のコード</h2>
                <!-- 出力用テキストエリア:読み取り専用で改善されたコードを表示 -->
                <textarea id="output" readonly></textarea>
                
                <!-- フォーマット実行ボタン -->
                <button onclick="formatCode()" id="formatButton">Format</button>
                <!-- ポップアップメッセージ表示エリア -->
                <div id="popup" class="popup">クリップボードにコピーしました</div>
            </div>
        </div>
    </div>
    <script src="./script.js"></script>
</body>
</html>

src\public\script.js

/**
 * ページ初期ロード時の処理
 * 
 * 【プロンプト選択の仕組み】
 * 1. ページロード時にプロンプト一覧を取得
 * 2. セレクトボックスの最初のオプションが自動的に選択される
 * 3. changePrompt()が呼ばれ、選択されたプロンプトの内容を表示
 * 
 * 【選択の優先順位】
 * 1. セレクトボックスの最初のオプション(データベースのORDER BY name順で最初のプロンプト)
 * 2. エラー時は「プロンプトの読み込みに失敗しました」というメッセージを表示
 */
window.onload = async function() {
    try {
        // プロンプトリストの取得
        const response = await fetch('/api/prompt-info');
        const buffer = await response.arrayBuffer();
        const decoder = new TextDecoder('utf-8');
        const jsonString = decoder.decode(buffer);
        const data = JSON.parse(jsonString);
        
        if (data.success) {
            // セレクトボックスの生成
            // ORDER BY name でソートされたプロンプトリストの最初のものが自動選択される
            const select = document.getElementById('promptSelect');
            select.innerHTML = data.prompts.map(prompt => 
                `<option value="${prompt.id}">${prompt.name}</option>`
            ).join('');

            // 最初のプロンプトの内容を取得して表示
            await changePrompt();
        }
    } catch (error) {
        console.error('初期化エラー:', error);
        document.querySelector('.prompt-display').textContent = 'プロンプトの読み込みに失敗しました';
    }
};

// プロンプト切り替え関数
async function changePrompt() {
    try {
        const selectedPrompt = document.getElementById('promptSelect').value;
        console.log('Selected prompt ID:', selectedPrompt); // デバッグログ

        const response = await fetch(`/api/prompts/${selectedPrompt}`);
        const data = await response.json();
        console.log('API Response:', data); // デバッグログ
        
        const promptDisplay = document.querySelector('.prompt-display');
        if (data.success) {
            promptDisplay.textContent = data.content;
        } else {
            promptDisplay.textContent = 'プロンプトの読み込みに失敗しました';
        }
    } catch (error) {
        console.error('プロンプト取得エラー:', error);
        document.querySelector('.prompt-display').textContent = 'プロンプトの読み込みに失敗しました';
    }
}

/**
 * コードのフォーマットを実行する関数
 * 1. 入力を取得
 * 2. サーバーにリクエスト
 * 3. 結果を表示
 */
async function formatCode() {
    const input = document.getElementById('input').value;
    const promptId = document.getElementById('promptSelect').value;
    const formatButton = document.getElementById('formatButton');
    
    // デバッグログ追加
    console.log('Formatting with:', {
        promptId,
        code: input
    });

    if (!input) {
        alert('コードを入力してください');
        return;
    }

    formatButton.innerHTML = '<span class="processing"><span></span><span></span><span></span></span>';

    try {
        const response = await fetch('/format', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ 
                code: input,
                promptId: promptId
            })
        });

        // デバッグログ追加
        console.log('Response status:', response.status);
        const data = await response.json();
        console.log('API Response:', data);
        
        if (data.success) {
            document.getElementById('output').value = data.formatted;
            formatButton.innerHTML = '分析完了! クリックしてコピー<i class="fa-regular fa-copy fa-beat-fade fa-lg" style="vertical-align: 0;"></i>';
            formatButton.onclick = copyToClipboard;
        } else {
            formatButton.textContent = 'エラー: ' + data.error;
        }
    } catch (error) {
        console.error('フォーマットエラー:', error);
        formatButton.textContent = 'エラー: ' + error.message;
    }
    formatButton.classList.remove('processing');
}

/**
 * クリップボードにコピーする関数
 * 出力用テキストエリアの内容をクリップボードにコピー
 */
 function copyToClipboard() {
    const outputTextarea = document.getElementById('output');
    outputTextarea.select();
    document.execCommand('copy');

    // ポップアップメッセージを表示
    const popup = document.getElementById('popup');
    popup.classList.add('show');

    // 一定時間後にポップアップメッセージを非表示にする
    setTimeout(() => {
        popup.classList.remove('show');
    }, 2000); // 2秒後に非表示
}

/**
 * ファイル選択時の処理
 * 選択されたファイルの内容を入力エリアに表示
 */
document.getElementById('vbaFile').addEventListener('change', (e) => {
    // 選択されたファイルを取得
    const file = e.target.files[0];
    if (file) {
        // FileReaderを使用してファイルの内容を読み込み
        const reader = new FileReader();
        // ファイル読み込み完了時の処理
        reader.onload = (e) => {
            // 読み込んだ内容を入力エリアに設定
            document.getElementById('input').value = e.target.result;
        };
        // ファイルをテキストとして読み込み開始
        reader.readAsText(file);
    }
});

// プロンプトの保存
async function savePrompt() {
    try {
        const promptId = document.getElementById('promptSelect').value;
        const content = document.getElementById('promptDisplay').textContent;
        
        console.log('Saving prompt:', promptId);
        console.log('Content:', content);

        const saveButton = document.getElementById('saveButton');
        saveButton.textContent = '保存中...';
        saveButton.disabled = true;

        // URLを確認のため出力
        const url = `/prompts/${promptId}`;
        console.log('Request URL:', url);

        const response = await fetch(url, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                content: content
            })
        });

        console.log('Response status:', response.status);
        const data = await response.json();
        console.log('Response data:', data);
        
        if (data.success) {
            alert('保存しました');
            document.getElementById('editButton').click();
        } else {
            alert('保存に失敗しました: ' + data.error);
        }

    } catch (error) {
        console.error('保存エラー:', error);
        alert('エラーが発生しました: ' + error.message);
    } finally {
        const saveButton = document.getElementById('saveButton');
        saveButton.textContent = '保存';
        saveButton.disabled = false;
    }
}

// 編集モードの切り替え
function toggleEdit() {
    const promptDisplay = document.getElementById('promptDisplay');
    const editButton = document.getElementById('editButton');
    const saveButton = document.getElementById('saveButton');
    const isEditing = promptDisplay.contentEditable === 'true';

    if (isEditing) {
        // 編集モード終了
        promptDisplay.contentEditable = 'false';
        editButton.textContent = '編集';
        saveButton.style.display = 'none';
        promptDisplay.classList.remove('editing');
    } else {
        // 編集モード開始
        promptDisplay.contentEditable = 'true';
        editButton.textContent = 'キャンセル';
        saveButton.style.display = 'block';
        promptDisplay.classList.add('editing');
        
        // カーソルを末尾に設定
        const range = document.createRange();
        const sel = window.getSelection();
        range.selectNodeContents(promptDisplay);
        range.collapse(false);
        sel.removeAllRanges();
        sel.addRange(range);
        promptDisplay.focus();
    }
}

// データ取得時の文字コード処理
async function fetchPromptInfo() {
  try {
    const response = await fetch('/api/prompt-info');
    const buffer = await response.arrayBuffer();
    const decoder = new TextDecoder('utf-8');
    const jsonString = decoder.decode(buffer);
    return JSON.parse(jsonString);
  } catch (error) {
    console.error('データ取得エラー:', error);
    throw error;
  }
}

src/public/style.css

body {
  font-family: "Noto Sans JP", serif;
  background-color: #f1f2f3;
}
/* メインコンテナ */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 2rem 2rem;
}

h1 {
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  text-align: center;
  letter-spacing: 0.1em;
  padding: 1rem 0;
  margin: 0 calc(50% - 50vw);
  width: 100vw;
  margin-bottom: 2rem;
}

h2 {
  margin-bottom: 1rem;
  text-align: center;
  letter-spacing: 0.1em;
}

.nav-links {
  text-align: center;
  margin-bottom: 2rem;
}

.nav-links a {
  display: inline-block;
  padding: 0.5rem 1rem;
  background-color: #e9be3b;
  color: white;
  text-decoration: none;
  border-radius: 4px;
  margin: 0 0.5rem;
}

.nav-links a:hover {
  background-color: #d3b458;
}

/* AIへの指示内容表示部分 */
.prompt-section {
  margin-bottom: 2rem;
}

.prompt-selector {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin-bottom: 1rem;
}

.edit-button {
  background-color: #4a90e2;
  height: 2.5rem;
}

.edit-button:hover {
  background-color: #357abd;
}

.save-button {
  display: none;
  margin: 1rem auto;
  background-color: #4caf50;
  width: 200px;
}

.save-button:hover {
  background-color: #45a049;
}

.prompt-display[contenteditable="true"] {
  border: 2px solid #4a90e2;
  padding: 1rem;
  outline: none;
}

.prompt-selector select {
  padding: 0.5rem 2rem 0.5rem 1rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  background-color: white;
  font-size: 1rem;
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: right 0.7rem center;
  background-size: 1em;
}

.prompt-selector select:hover {
  border-color: #888;
}

.prompt-selector select:focus {
  outline: none;
  border-color: #e9be3b;
  box-shadow: 0 0 0 2px rgba(233, 190, 59, 0.2);
}

.prompt-display-container {
  display: flex;
  align-items: end;
  justify-content: center;
  gap: 2rem;
  margin: 0 auto;
}

.user-icon {
  width: 40px;
  height: 40px;
  background-color: #f6ce55;
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 3rem;
  position: relative;
}

.user-icon::after {
  content: "";
  position: absolute;
  top: 96%;
  left: 50%;
  transform: translateX(-50%);
  width: 50px;
  height: 40px;
  background-color: #f6ce55;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  border-top-left-radius: 50%;
  border-top-right-radius: 50%;
}

.prompt-display-wrapper {
  max-width: 80%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #fff;
  border-radius: 5rem 5rem 5rem 0;
  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
  padding: 2rem;
}

.prompt-display {
  font-family: monospace;
  font-size: 0.825rem;
  line-height: 1.5;
  white-space: pre-wrap;
  max-height: 400px;
  min-height: 300px;
  min-width: 600px;
  height: auto;
  overflow-y: auto;
  resize: vertical;
  padding: 1rem;
}

/* カスタムスクロールバーのスタイル */
.prompt-display::-webkit-scrollbar {
  width: 0.5rem; /* スクロールバーの幅 */
}

.prompt-display::-webkit-scrollbar-thumb {
  background: #888;
  border-radius: 1rem;
}

/* エディタ部分 */
.editors {
  display: flex;
  gap: 2rem;
  margin-bottom: 1rem;
  position: relative;
}

.editor {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.editor > * {
  margin-bottom: 1rem;
}

.editor > *:last-child {
  margin-top: auto !important;
}

.editor input {
  height: 3rem;
  background-color: #ccc;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  width: 100%;
  transition: all 0.3s ease;
}

.editor input:hover {
  background-color: #bbb;
}

.editor .detail {
  margin-top: 0.5rem;
  color: #ff565d;
}

/* テキストエリア */
.editor textarea {
  width: 100%;
  height: 500px;
  font-family: monospace;
  padding: 10px;
  font-size: 14px;
  line-height: 1.5;
  border: 2px solid #ccc;
  background-color: #fff;
  border-radius: 4px;
}

button {
  height: 3rem;
  padding: 0.5rem 1rem;
  background-color: #e9be3b;
  border-radius: 4px;
  color: white;
  letter-spacing: 0.1em;
  border: none;
  cursor: pointer;
  position: relative;
}

button:hover {
  background-color: #d3b458;
  transition: all 0.3s ease;
}

/* アニメーションを追加するクラス */
.processing {
  display: inline-flex;
  gap: 0.5rem;
}

.processing span {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  background-color: #fff;
  border-radius: 50%;
  animation: fadeDots 1.5s infinite ease-in-out;
}

/* それぞれのドットに異なるアニメーションの遅延を設定 */
.processing span:nth-child(1) {
  animation-delay: 0s;
}

.processing span:nth-child(2) {
  animation-delay: 0.2s;
}

.processing span:nth-child(3) {
  animation-delay: 0.4s;
}

/* 薄さ(透明度)を変化させるアニメーション */
@keyframes fadeDots {
  0%,
  100% {
    opacity: 0.2;
  } /* 薄くなる */
  50% {
    opacity: 1;
  } /* 濃くなる */
}

.popup {
  visibility: hidden; /* 初期状態では非表示 */
  position: absolute;
  bottom: 4.5rem;
  right: 0.5rem;
  background-color: rgba(76, 175, 80, 0.8);
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  color: white;
  letter-spacing: 0.1em;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  z-index: 1000;
  opacity: 0;
  transform: translateY(1rem);
  transition: opacity 0.5s ease, transform 0.5s ease, visibility 0.5s;
}

.popup.show {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
}

src\public\about.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>About VBA Formatter</title>
    <!-- <link rel="stylesheet" href="https://unpkg.com/ress/dist/ress.min.css" /> -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
    <style>
        .section {
            background: white;
            border-radius: 8px;
            padding: 2rem;
            margin-bottom: 2rem;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .prompt-card {
            border: solid 2px #ccc;
            border-radius: 8px;
            padding: 2rem;
            margin-bottom: 2rem;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .prompt-content {
            background: #f8f9fa;
            padding: 1.5rem;
            border-radius: 4px;
            white-space: pre-wrap;
            font-family: monospace;
            font-size: 0.825rem;
            line-height: 1.5;
            overflow-x: auto;
        }

        .prompt-footer {
            margin-top: 1rem;
            padding-top: 1rem;
            border-top: 1px solid #eee;
            color: #666;
            font-size: 0.9rem;
        }

        .error {
            color: #dc3545;
            padding: 1rem;
            background: #fff;
            border-left: 4px solid #dc3545;
            margin: 1rem 0;
        }
    </style>
</head>
<body>
    <h1>VBA Formatter について</h1>

    <div class="container">
        <div class="nav-links">
            <a href="index.html">ホーム</a>
            <a href="about.html">About</a>
        </div>

        <!-- プロンプトセクション -->
        <div class="section">
            <h2>プロンプト管理</h2>
            <div id="promptInfo" class="prompt-info">
                <!-- プロンプト情報がここに動的に挿入されます -->
                <div class="loading">読み込み中...</div>
            </div>
        </div>

        <div class="section">
            <h2>システム概要</h2>
            <p>
                Code Formatterは、コードを自動的に整形し、プロンプトに従ってコードを改善するツールです。
                <br>Docker環境で動作し、Node.jsとMySQLを利用しています。
            </p>
        </div>

        <div class="section">
            <h2>Docker環境について</h2>
            <ul class="point-list">
                <li>コンテナの停止(docker-compose down): データは保持されます</li>
                <li>Docker Desktop停止: データは保持されます</li>
                <li>ボリューム削除(docker-compose down --volumes): データは消失します</li>
            </ul>
        </div>

        <div class="section">
            <h2>使用方法</h2>
            <ul class="point-list">
                <li>プロンプトの選択: ドロップダウンメニューから選択</li>
                <li>編集: 「編集」ボタンをクリックして内容を修正</li>
                <li>保存: 「保存」ボタンをクリックしてデータベースに保存</li>
                <li>確認: 保存完了のメッセージを確認</li>
            </ul>
        </div>

        <div class="section">
            <h2>開発者向け情報</h2>
            <ul class="point-list">
                <li>デバッグ: docker-compose logsでログを確認できます</li>
                <li>データベース確認:<br>
                    - docker exec -it 1be476fccbd035b76a53810a40486a0a427266c50deb5b681671dcc2557ca5be bashで接続<br>
                    - mysql -u root -p</li>
                <li>ボリューム確認: docker volume inspectで詳細を確認</li>
            </ul>
        </div>
    </div>
    <script>
        // プロンプトの表示用スクリプト
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

function formatDate(dateStr) {
    return new Date(dateStr).toLocaleString('ja-JP', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit'
    });
}

async function loadPromptInfo() {
    try {
        const response = await fetch('/api/prompt-info');
        const data = await response.json();
        
        if (data.success) {
            const promptsHtml = data.prompts.map(prompt => `
                <div class="prompt-card">
                    <h3>${escapeHtml(prompt.name)}</h3>
                    <p class="description">${escapeHtml(prompt.description)}</p>
                    <pre class="prompt-content">${escapeHtml(prompt.content)}</pre>
                    <div class="prompt-footer">
                        最終更新: ${formatDate(prompt.updated_at)}
                    </div>
                </div>
            `).join('');
            
            document.getElementById('promptInfo').innerHTML = promptsHtml;
        }
    } catch (error) {
        console.error('プロンプト情報の取得に失敗:', error);
        document.getElementById('promptInfo').innerHTML = 
            '<div class="error">プロンプト情報の取得に失敗しました</div>';
    }
}

document.addEventListener('DOMContentLoaded', loadPromptInfo);
    </script>
</body>
</html>

日本語が文字化けする問題

  1. 関連するファイルを共有:
  • docker-compose.yml
  • connection.js
  • index.js
  • package.json
  • エラーログやスクリーンショット
  1. これまでに試したこと:
  • DBの文字コード設定確認(utf8mb4)
  • Bufferを使用したデコード処理 など

そして、先ほどのnpm installについては、Dockerコンテナ内で実行する場合は:

試したこと:

DBの文字コード設定確認

  • 文字セットが全て utf8mb4 であることを確認
  • collation が utf8mb4_unicode_ci であることを確認

connection.js での試み

  • charset の指定
  • collation, encoding の追加(警告が出たため無効)
  • typeCast オプションの追加

index.js での試み

  • Buffer を使用したデコード処理
  • Content-Type ヘッダーの設定
  • HEX形式でのデータ取得

データ確認

  • DBに直接アクセスして日本語データが正常なことを確認
  • hex形式でのデータ内容確認

日本語が文字化けする問題

文字コードとは

  • コンピュータが文字を扱うための約束事
  • 例:「あ」という文字を16進数の E38182 として表現
  • 主な文字コード:
    • UTF-8:日本語を含む世界中の文字を扱える(最も一般的)
    • latin1:英語圏で使用される基本的な文字セット
    • Shift-JIS:日本語Windows向けの文字コード

多重エンコードとは

  • 既にエンコード(変換)された文字を、さらに変換してしまう状態
  • 例:
    1. 「あ」→ E38182(1回目の変換:正常)
    2. E38182C3A3E28182(2回目の変換:不要)
  • この状態だと文字化けの原因となる

現在の問題

  • DBに保存する際に多重エンコードが発生
  • Node.jsでデータを取得する際に文字化けして表示
文字「あ」 UTF-8エンコード E38182 文字「あ」 1回目のエンコード E38182 2回目のエンコード C3A3E28182 正常な変換 多重エンコード(問題のある状態)

試したアプローチ:

  1. アプリケーション層での対応:
    • Content-Type設定
    • Buffer経由のデコード
    • iconv-liteでのデコード
  2. DB層での対応:
    • CONVERTを使用した変換
    • CAST AS BINARYの使用
    • HEX形式での取得
  3. 接続設定での対応:
    • charsetの設定
    • エンコーディングオプション
    • セッション文字コードの設定

未試行と考えられるアプローチ:

  1. DB接続時のクエリ結果処理:
SSH接続して別端末でDockerコマンドを使用
ssh ユーザー名@IPアドレス
  • ポート番号は鍵、デフォルト22
    指定の番号でのみ開放し許可することもできる

ポートフォワーディング

SSH接続のポートフォワーディングとは、SSHトンネルを利用して、リモートサーバーとローカルホストの間で通信を安全に中継する技術です。これにより、ローカルまたはリモートの特定のポートで動作するサービスにアクセスすることができます。

ローカルポートフォワーディングローカルマシンのポートを介して、リモートサーバー上の特定のポートにアクセスします。ファイアウォールで制限されているリモートサービスにアクセスする。
VPNを使わずにリモートリソースを利用する。
リモートポートフォワーディングリモートサーバー側のポートを介して、ローカルマシンまたは別のリモートホスト上の特定のポートにアクセスします。ローカルホスト上で動作するサービスを、リモートサーバーからアクセス可能にする。
ローカル開発環境をリモートユーザーに提供する。
ダイナミックポートフォワーディングプロキシサーバーとしてSSHを使用し、複数の宛先にトンネルを作成します。これはSOCKSプロキシとして機能します。セキュアなブラウジング(例えば、特定の国で制限されているサイトにアクセス)。
リモートネットワークの複数サービスを一度に利用。

プロキシサーバーのフィルターが原因?アクセスできないサイトを突き止める方法、自作拡張機能で確認

はじめに

社内ネットワークや学校などの環境では、インターネットアクセスの管理・セキュリティ強化のためにプロキシサーバーが導入されていることがよくあります。ときにはこのプロキシサーバーのフィルタリング機能が原因で、特定のWebサイトやサービスにアクセスできなくなることも。

プロキシサーバーとは

代理サーバーとしての役割

プロキシサーバー(proxy server)とは、ユーザーのリクエスト(通信)を“代理”として受け取り、インターネット上の目的のサーバーにアクセスする役割を担うサーバーのことです。
一般的に、以下の目的で利用されます。

キャッシュ機能: よくアクセスするサイトのデータをプロキシ側で一時保存し、ネットワーク帯域を節約する。
セキュリティ・フィルタリング: 社内ルールや企業ポリシーに反するサイトや有害サイトへのアクセスを制限する。
ログ管理: どの端末がどのサイトにアクセスしているかを記録・監視する。

フィルタリングの仕組み

プロキシサーバーにはURLやキーワードをもとにアクセスを許可・ブロックする機能が搭載されている場合があります。たとえば「SNSサイト禁止」「特定のカテゴリのサイトは禁止」のように設定されている場合、該当のサイトにアクセスしようとすると、ブラウザ上にブロック画面が表示されたり“接続エラー”になったりすることがあります。

プロキシサーバーのフィルターでサイトにアクセスできないケース

よくある症状

プロキシサーバーが原因で特定のサイトにアクセスできないとき、以下のような症状が現れることが多いです。

接続タイムアウト: ブラウザ上でエラーが表示され、ページがまったく表示されない。
ブロック画面が表示される: 「このサイトはアクセス禁止です」などの専用ページが表示される。
部分的なリソースの読み込み失敗: サイトは見られるが、画像や特定のスクリプトだけが読み込めない。
こういった場合、プロキシサーバーの管理画面を確認できれば、どのURLがブロックされたのか一目でわかることがあります。しかし、組織の方針などで管理画面にアクセスできないケースも少なくありません。

検証ツールを使って原因(ホワイトリスト)を突き止める手順

ここでは、プロキシサーバーの管理画面に頼らずに、Chromeの開発者ツール(検証ツール)などを使ってフィルタリングの原因を調べる方法を紹介します。
他のブラウザ(Firefox、Edgeなど)でも同様の手順で確認できますが、ここではChromeを例とします。

開発者ツール(検証画面)の起動

Chromeを開き、アクセスしたいサイトに移動。
キーボードのF12キー、または右上のメニューから「その他のツール > デベロッパーツール」を選択。

Networkタブの確認

開発者ツールを開いたら、上部のタブからNetworkを選択する。
すでにページが読み込まれている場合は、いったんリロード(もしくは「Disable cache」にチェックを入れてリロード)する。
すると、該当サイトの読み込みリクエストの一覧が表示される。
ここで注目すべきは、

ステータスコード: 200(OK)、404(Not Found)、403(Forbidden)、407(Proxy Authentication Required)など
URL: ブロックの原因となっているかもしれないドメインやファイルパス
もしフィルタリングが作動している場合、特定のリソースだけ403や404、あるいは proxy error が返っているといったことが確認できるでしょう。

Consoleタブのエラーを確認

Networkタブにエラーがない場合でも、Consoleタブにエラーメッセージが残っていることがあります。

ブロックされたリソースのURL
「Failed to load resource: the server responded with a status of 403」などの文言
これらのエラー文言から、フィルタリングによるエラーかどうか判断材料になります。

実際にブロックされているURLをブラウザで直接開いてみる

NetworkタブやConsoleタブで見つけたエラーURLに対して、

ブラウザのアドレスバーに直接貼り付けてアクセス
「このURL自体がブロックされているのか」を確認
これにより、主サイト自体は表示できても、広告や解析用のドメインがブロックされてページが中途半端に崩れているというケースも特定できるはずです。

補足:コマンドラインの活用例

もし開発者ツールだけでは原因が分かりづらい場合、curlやpingなどのコマンドラインツールを使っても検証ができます。

curlでの確認

ターミナルやコマンドプロンプトで下記のように入力します:

curl -I http://アクセスしたいサイトのURL

ヘッダー情報(ステータスコードなど)が返ってきますが、もしプロキシが介入している場合は「プロキシのエラーメッセージ」「Access Denied」などが返ってくることがあります。

pingでの接続確認

ping アクセスしたいサイトのドメイン

pingはICMPプロトコルを使っているため、Webアクセスのフィルタリングと挙動が異なることもありますが、サーバー側に全く応答がないのか、そもそもDNS解決できないのか、といった点を切り分けるヒントになります。

adobe ホワイトリスト プロキシサーバー 拡張機能 参照URL一覧

自作Chromeの拡張機能でリソースURLリストを取得

Webページが読み込む全てのリソース(画像、スクリプト、スタイルシート、API呼び出しなど)のURLを取得

構成ファイル

page-resources-monitor/
├── manifest.json      # 拡張機能の設定ファイル
├── background.js      # バックグラウンドでリクエストを監視
├── popup.html         # ポップアップのUI
└── popup.js          # ポップアップの動作制御

manifest.json

{
  "manifest_version": 3,
  "name": "Page Resources Monitor",
  "version": "1.0",
  "description": "ページが読み込むすべてのリソースのURLを表示します",
  "permissions": [
    "activeTab",
    "declarativeNetRequest",
    "scripting",
    "webRequest",
    "webNavigation"
  ],
  "host_permissions": [
    "*://*/*",
    "https://*/*"
  ],
  "action": {
    "default_popup": "popup.html"
  },
  "background": {
    "service_worker": "background.js",
    "type": "module"
  }
}

background.js

let requests = new Map();

function addRequest(tabId, requestData) {
  if (!requests.has(tabId)) {
    requests.set(tabId, []);
  }
  requests.get(tabId).push(requestData);
}

chrome.webRequest.onBeforeRequest.addListener(
  function(details) {
    console.log('Captured request:', details);
    if (details.tabId !== -1) {
      const requestData = {
        url: details.url,
        type: details.type,
        method: details.method || 'GET',
        timestamp: details.timeStamp,
        initiator: details.initiator || '',
        frameId: details.frameId || 0,
        parentFrameId: details.parentFrameId || 0,
        documentUrl: details.documentUrl || ''
      };
      
      addRequest(details.tabId, requestData);
    }
  },
  {
    urls: ["<all_urls>"],
    types: [
      "main_frame",
      "sub_frame",
      "stylesheet",
      "script",
      "image",
      "font",
      "object",
      "xmlhttprequest",
      "ping",
      "csp_report",
      "media",
      "websocket",
      "other"
    ]
  }
);

// タブが切り替わった時のリクエスト情報保持
chrome.tabs.onActivated.addListener(function(activeInfo) {
  if (!requests.has(activeInfo.tabId)) {
    requests.set(activeInfo.tabId, []);
  }
});

// メッセージリスナー
chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    if (request.action === "getRequests") {
      chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
        if (tabs && tabs[0]) {
          const currentTabId = tabs[0].id;
          const tabRequests = requests.get(currentTabId) || [];
          sendResponse({requests: tabRequests});
        } else {
          sendResponse({requests: []});
        }
      });
      return true;
    }
  }
);

popup.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <style>
      body {
        width: 800px;
        padding: 10px;
      }
      .resource-list {
        max-height: 600px;
        overflow-y: auto;
      }
      .resource-item {
        margin: 5px 0;
        padding: 5px;
        border-bottom: 1px solid #eee;
      }
      .type-filter {
        margin: 10px 0;
      }
      .url {
        word-break: break-all;
      }
      .type {
        color: #666;
        font-size: 0.9em;
      }
      button {
        margin: 5px;
        padding: 8px 16px;
        background-color: #4caf50;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
      }
      button:hover {
        background-color: #45a049;
      }
      #exportCSV {
        background-color: #2196f3;
      }
      #exportCSV:hover {
        background-color: #1976d2;
      }
      #exportJSON {
        background-color: #ff9800;
      }
      #exportJSON:hover {
        background-color: #f57c00;
      }
      .stats {
        margin: 10px 0;
        padding: 10px;
        background: #f5f5f5;
      }
    </style>
  </head>
  <body>
    <h3>ページリソース一覧</h3>
    <div class="type-filter">
      <label>リソースタイプでフィルター:</label>
      <select id="typeFilter">
        <option value="all">すべて</option>
        <option value="script">スクリプト</option>
        <option value="stylesheet">スタイルシート</option>
        <option value="image">画像</option>
        <option value="font">フォント</option>
        <option value="xmlhttprequest">API/XHR</option>
        <option value="websocket">WebSocket</option>
        <option value="other">その他</option>
      </select>
    </div>
    <div class="stats" id="stats"></div>
    <button id="copyButton">URLリストをコピー</button>
    <button id="exportCSV">CSVでエクスポート</button>
    <button id="exportJSON">JSONでエクスポート</button>
    <div id="resourceList" class="resource-list"></div>
    <script src="popup.js"></script>
  </body>
</html>

popup.js

document.addEventListener('DOMContentLoaded', function() {
    const resourceList = document.getElementById('resourceList');
    const typeFilter = document.getElementById('typeFilter');
    const stats = document.getElementById('stats');
    const copyButton = document.getElementById('copyButton');
    let allRequests = [];
  
    function updateList(requests, filterType = 'all') {
      resourceList.innerHTML = '';
      
      if (!Array.isArray(requests)) {
        console.error('requests is not an array:', requests);
        requests = [];
      }
  
      const filteredRequests = filterType === 'all' 
        ? requests 
        : requests.filter(r => r.type === filterType);
  
      filteredRequests.forEach(request => {
        const div = document.createElement('div');
        div.className = 'resource-item';
        div.innerHTML = `
          <div class="url">${request.url}</div>
          <div class="type">タイプ: ${request.type} (${request.method || 'GET'})</div>
          <div class="initiator">発信元: ${request.initiator || '不明'}</div>
        `;
        resourceList.appendChild(div);
      });
  
      const typeCount = requests.reduce((acc, r) => {
        acc[r.type] = (acc[r.type] || 0) + 1;
        return acc;
      }, {});
  
      stats.innerHTML = `
        <strong>統計情報:</strong><br>
        総リクエスト数: ${requests.length}<br>
        タイプ別: ${Object.entries(typeCount)
          .map(([type, count]) => `${type}: ${count}`)
          .join(', ')}
      `;
    }
  
    function refreshData() {
      chrome.runtime.sendMessage({action: "getRequests"}, function(response) {
        console.log('Received response:', response);
        if (response && response.requests) {
          allRequests = response.requests;
          updateList(allRequests, typeFilter.value);
        }
      });
    }
  
    refreshData();
    setInterval(refreshData, 3000);
  
    typeFilter.addEventListener('change', function() {
      updateList(allRequests, this.value);
    });
  
    copyButton.addEventListener('click', function() {
      const urls = allRequests.map(r => r.url).join('\n');
      navigator.clipboard.writeText(urls).then(function() {
        copyButton.textContent = 'コピーしました!';
        setTimeout(function() {
          copyButton.textContent = 'URLリストをコピー';
        }, 2000);
      });
    });
  });

拡張機能のインストール方法:

  • Chrome で chrome://extensions/ を開きます
  • 右上の「デベロッパーモード」を有効にします
  • 上記のフォルダを「パッケージ化されていない拡張機能を読み込む」でインポートします

主な処理の特徴:

  1. バックグラウンドプロセス(background.js)
  • 常時動作してリクエストを監視
  • Mapデータ構造でタブごとにリクエストを保持
  • メッセージングAPIでポップアップとデータをやり取り
  1. ポップアップUI(popup.html/js)
  • ユーザーインターフェースの提供
  • リアルタイムデータ更新
  • フィルタリングと表示制御
  • データエクスポート機能
background.js Webリクエスト監視 データ保存 popup.html/js UI表示 データ取得・更新 データ取得 Webブラウザ リクエスト情報

WebRequest API

WebRequest APIは、Chrome拡張機能がブラウザのネットワークリクエストを監視・分析できるAPIです。

取得できる情報:

  • URLやメソッド
  • リクエスト/レスポンスヘッダー
  • リクエストタイプ(image, script, stylesheet等)
  • リクエスト元のタブID
オリジナルWordPressテーマの修正・バックアップ、コンテンツをDBから取得(DBの置換には「Search Replace DB」)

WordPressのオリジナルテーマを外注して納品してもらった場合でも、ちょっとしたテキスト修正やデザイン調整など軽微な変更は必要になることがあります。
本記事では、管理画面での一般的な修正方法と、万が一に備えてのバックアップ方法についてまとめました。

テーマエディターでの軽微な修正

WordPress管理画面の「外観 > テーマファイルエディター」から直接テーマファイルを編集することも可能です。
ただし、誤って重要なコードを消してしまうリスクや、運用中サイトが即時に壊れてしまう可能性があるため、以下の点に注意してください。

  • 必ずバックアップを取得してから編集
  • PHPファイル編集の際はテスト環境で確認後に本番に反映する

【おすすめ】子テーマを使うメリット

外注テーマがアップデートで修正される可能性がある場合、子テーマを作成してカスタマイズを行うのがおすすめです。
子テーマを利用すると、親テーマ(オリジナルテーマ)のアップデートがあっても、子テーマ部分の修正は上書きされずに済むため、作業の手戻りを減らせます。

子テーマ作成の流れ(簡易版):

  1. wp-content/themes 配下に、子テーマ用フォルダを新規作成 (例: originaltheme-child)
  2. style.cssfunctions.php を設置し、style.css の先頭に以下を追記
  3. functions.php で親テーマのスタイルを読み込む記述を行う
  4. WordPress管理画面から「外観」→「テーマ」で子テーマを有効化
/*
Theme Name: Original Theme Child
Template: originaltheme
*/

バックアップ方法

WordPressサイトでは、万が一に備えて定期的なバックアップが重要です。以下は代表的な方法になります。

プラグインを使ったバックアップ

プラグインを利用することで、自動バックアップや復元がGUI操作で簡単に行えるようになります。
代表的なプラグインと機能の比較は下記の通りです。

プラグイン名無料/有料特徴復元方法
UpdraftPlus無料 / 有料手動&自動バックアップ対応、クラウド連携可ダッシュボードから可
BackWPup無料 / 有料定期実行スケジュール設定が豊富手動インポートが必要
All-in-One WP Migration無料 / 有料サイト移行もできるバックアップ機能プラグインから一括復元

サーバーから手動でバックアップを取得する

サーバーコントロールパネルやFTPソフトを使って手動でファイルとデータベースをダウンロードする方法です。

  1. FTPでファイルをダウンロード
    • WordPressフォルダ全体(wp-content など含む)
  2. phpMyAdminなどでデータベースをエクスポート
    • .sql ファイルをローカルにダウンロード

メリット

  • プラグインに依存しないためトラブル時に確実性が高い
  • より細かい手動制御ができる

デメリット

  • 作業手順がやや煩雑
  • 自動スケジュールが無いので定期的な手動作業が必要

Gitなどのバージョン管理システムの活用

テーマファイルやプラグインコードの変更が多い場合には、Gitなどを使ってバージョン管理しておくと便利です。

  • 変更履歴を明確に把握できる
  • ローカル環境でブランチを分けて開発・テストができる
  • 復元ポイントが細かく残る

修正から公開までのフロー例

  1. バックアップ取得: 現状の動作しているサイト状態を念のため保存
  2. テスト環境で修正: ローカルやステージング環境でテーマ・プラグインなどを編集
  3. 動作確認: バグや不具合がないかチェック
  4. 本番環境へ反映: 変更を本番に適用
  5. 再度バックアップ: 修正後の最新状態も保存

ローカル開発環境構築手順

前提:オリジナルテーマはGithubで管理

本番:さくらレンタルサーバー

WordPress開発環境をDockerで構築

WordPressの本番環境からローカル環境へコンテンツを同期する方法

WordPressデータベース同期方法の比較

【プラグイン】All-in-One WP Migration・サイズ制限があり(無料版)、小規模サイト向け
・ユーザーデータも含めて移行される
phpMyAdmin・MySQLをブラウザ上で管理するためのツール
WP CLI・CLI(コマンドラインインターフェース)

本番サイトのDBをエクスポート

エクスポートとはデータベースの中身を「SQLファイル」として保存することです。

本番のさくらインターネットのレンタルサーバーにssh接続します

wp-cliを使うには、WordPressのルートディレクトリで実行する必要があります。

$ wp db size

$ wp db tables
wp_hello_actionscheduler_actions
wp_hello_actionscheduler_claims 
wp_hello_actionscheduler_groups 
wp_hello_actionscheduler_logs   
wp_hello_commentmeta
wp_hello_comments
wp_hello_links
wp_hello_options
wp_hello_postmeta
wp_hello_posts
wp_hello_term_relationships     
wp_hello_term_taxonomy
wp_hello_termmeta
wp_hello_terms
wp_hello_usermeta
wp_hello_users

ホームディレクトリ(公開ディレクトリでない)場所にエクスポート

# ホームディレクトリに移動
cd ~

# バックアップ用ディレクトリ作成
mkdir db_backup
[xx@wwwxx ~/www/wp]$ wp db export --tables=wp_hello_posts,wp_hello_postmeta,wp_hello_terms,wp_hello_term_taxonomy,wp_hello_term_relationships,wp_hello_options ~/db_backup/content_backup.sql

Search Replace DB

WordPressのデータベースを簡単に検索・置換するためのPHPスクリプトです。WordPressサイトを移行する際や、URLやパスを変更する必要がある場合に非常に便利です。

使用方法

移動元WordPressの準備

レンタルサーバの管理画面からphpMyAdminを開きます

(ログイン情報wp-config.phpに記載されています)

使用しているDBを選択します

wp~で始まるテーブル名の一覧が表示されている状態を確認(テーブル名がwp4b164fで始まっているのは、WordPressインストール時に設定されたテーブル接頭辞($table_prefix)が適用されてるためです)

phpMyAdminの画面上部メニューにある「エクスポート」を選択します

  • 簡易 – 最小限のオプションだけ表示
    → 初心者向けで必要十分な設定がデフォルトで適用されます。特別な設定やカスタマイズが必要ない場合、この方法を選択してください。
  • なし
    これを選択してください。
    WordPressのデータベースは通常utf8mb4utf8エンコーディングを使用しており、エンコーディングを変更すると文字化けの原因になる可能性があります。

移動元のWordPressフォルダ、ファイル群をダウンロードし移動先にアップロード

移動先にアップロードする前にwp-config.phpのダウンロードを忘れずに

(注意)db接続情報まで書き換えてしまうと接続エラーになります

Search Replace DBダウンロード方法

https://interconnectit.com/search-and-replace-for-wordpress-databases

上記サイトよりフォームを送信すると、しばらくするとメールが送られてきます。

メールにZipファイルのダウンロードURLが記載されてます

Githubからダウンロードも可能

https://github.com/interconnectit/Search-Replace-DB

アップロード

Search Replace DB をWordPress移行先に配置

WordPressをインストールしているディレクトリに(wp-config.phpと同じ階層に配置)

検索と置換を設定

ブラウザでアクセスすると下記が表示されます(ディレクトリ名が長いので変更したほうが便利)

DB接続情報を「wp-config.php」より入力

DB_HOSTの値にポート番号が含まれていないため、デフォルトのMySQLポート番号(3306)が使用されます。

– phpMyAdminのDBインポートは圧縮しないとエラーがでる

ドライラン(Dry Run)

実行前に「Dry Run」モードで変更内容を確認します。

実行

問題がなければ検索・置換を実行します。

https://internet.mints.ne.jp

– DB置換後、移動元のURLに遷移する場合はキャッシュが原因の場合があります、シークレットモードで確認

Dockerコンテナで開発している場合の注意

– search db repはDockerfileでphpのバージョンを7.4に変更する必要がある?

FROM wordpress:php7.4-apache

– db情報はdocker-compose.ymlの内容を使用※wp-config.phpの内容は使用しない

WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_NAME: wordpress

参考サイト

 WordPress Search Replace DB の使い方
https://www.webdesignleaves.com/pr/wp/wp_search_replace_db.html

【WordPress】「Smash Balloon Instagram Feed」で発生する“Instagram フィードで重大な問題”エラーの対処法

WordPressでInstagramの投稿を簡単に表示できるプラグインといえば「Smash Balloon Instagram Feed(旧称:Instagram Feed)」が有名ですよね。しかし、アップデートやインスタグラムのAPI仕様変更などが原因で、たまに次のようなエラーに遭遇することがあります。

“instagram フィードで重大な問題”
もしくは
“Instagram Feed: There is a critical error on this site.”

このようなエラーが発生すると、Instagramの投稿がサイトに表示されなくなってしまうため、見栄えや運用上の問題も大きく、すぐに対処が必要となります。本記事では、このエラーの原因や具体的な対処方法、注意点をご紹介します。

WordPressの管理画面での確認


1. エラー内容:「instagram フィードで重大な問題」とは?

「Smash Balloon Instagram Feed」は、Instagram APIを通じて投稿を取得し、WordPressサイトに埋め込むプラグインです。エラー文として「instagram フィードで重大な問題」「Instagram Feed error」などが表示される場合、以下のような原因が考えられます。

  1. Instagramアクセストークンの期限切れ・再認証切れ
    • InstagramのAPIアクセスを許可するためのトークンが有効期限切れになっている可能性が高いです。
  2. プラグインやWordPress本体のバージョンが古い
    • 古いバージョンだとInstagram側のAPI仕様変更に追随していないケースがあります。
  3. アクセス制限(サーバーやセキュリティプラグインとの競合)
    • サーバーやセキュリティプラグインでInstagram APIへのリクエストがブロックされていることもあります。
  4. キャッシュの不整合
    • プラグイン内部のキャッシュが壊れており、Instagramから投稿を正しく取得できない場合があります。

参考サイト

ワードプレスプラグイン『Smash Balloon Instagram Feed』でエラーが出る…!エラーを消してインスタグラムをちゃんと表示させたい。
https://granvi.jp/marketing/smash-balloon-instagram-feed-error

2024年12月4日以降にInstagramフィードが表示できなくなったWordPressサイトの対応
https://lofir.net/instagram-feed-2024-12/

Instagramの投稿をホームページに埋め込む方法を分かりやすく解説
https://www.xserver.ne.jp/bizhp/insert-instagram/?gad_source=1&gclid=Cj0KCQiAvbm7BhC5ARIsAFjwNHs2b9fn9-6XWK4I16jy3SyiT_bZ2XrgxDj6bbsPdFY9sTeHOELCxNAaAvxEEALw_wcB

【Amazon ECS】Docker で開発した Web アプリケーションを AWS へデプロイする方法

全体の流れ

  1. Docker イメージの作成
  2. AWS リポジトリ(Amazon ECR)への Docker イメージのプッシュ
  3. Amazon ECS(Fargate)を使ったタスク定義・サービスの設定
  4. デプロイと運用

Node.js プロジェクトの場合

ローカル環境に Node.js をインストールせず、Docker コンテナ内で npm init や npm install を行うやり方です。
(最初にコンテナを作って、その中でプロジェクトを初期化し、あとからローカルにファイルを持ってくるイメージです。)

最低限の Dockerfile を作成

mkdir node-docker-init
cd node-docker-init

cat << 'EOF' > Dockerfile
FROM node:18-alpine

# 作業ディレクトリ設定(まだソースはコピーしない)
WORKDIR /usr/src/app

# とりあえずシェルでコンテナ起動できるようにする
CMD ["sh"]
EOF
Laravelでファイルアップロード

「ファイルのアップロード」がどういう流れで行われるのか?

  1. ブラウザ(クライアント)側:
<!-- ファイル送信用のフォーム -->
<form method="POST" enctype="multipart/form-data">
    <input type="file" name="uploadFile">
    <button>送信</button>
</form>

enctype="multipart/form-data" という部分がないとファイルを正しく送信できません。

なぜなら:

  • ファイルには画像やPDFなど、テキスト以外のデータ(バイナリデータ)が含まれます
  • そのため、特別な送信方式(multipart/form-data)が必要になります
  • ファイル送信:ヘッダー情報(メタデータ)とファイルデータが分かれて送信される
  • これは、ファイルには画像やPDFなど、テキスト以外のデータが含まれるため、特別な形式(multipart/form-data)が必要になるためです。

▼プレビュー

ユーザーがこのフォームで「ファイルを選択」して「アップロード」ボタンを押すと、選択したファイルがサーバーに送られます。

<ファイルアップロードのフロー>

<POSTリクエストの違い>

<サーバーサイドのフロー>

メモリ領域 一時ファイル保管 (バッファ) バリデーション – サイズチェック – 形式チェック ストレージ – ファイル保存 – 権限設定 エラーハンドリング エラー発生時の処理 処理の流れ: 1. ファイルデータをメモリに一時保管 2. バリデーションで各種チェック実行 3. 問題なければストレージに保存 4. エラー時は適切なエラーハンドリング
Feature BranchでのGitマージ戦略:安全なコード統合のためのベストプラクティス

Gitを使用したチーム開発において、feature branchとmain branchの統合は重要なポイントとなります。この記事では、安全かつ効率的なブランチ統合の戦略について解説します。

なぜFeature Branchで先にマージするのか?

Feature branchで開発を進める中で、main branchの変更を早めに取り込むことには、いくつかの重要な利点があります:

  1. コンフリクトの早期発見
  2. 段階的な統合テストが可能
  3. 最終的なmainブランチへの統合がスムーズ

具体的な手順

1. Mainの変更を取り込む

まず、feature branchで作業中に、main branchの変更を取り込みます:

# 現在のブランチを確認
git branch

# mainの変更を取り込む
git merge main

2. コンフリクト対応

この時点でコンフリクトが発生した場合の対処:

  1. コンフリクトが発生したファイルの確認
  2. 各ファイルのコンフリクトを解決
  3. 解決したファイルをステージング
git add <コンフリクトを解決したファイル>
  1. マージの完了
git commit -m "Resolve conflicts with main"
main feature mainの変更 feature開発 mainマージ& コンフリクト確認 最終マージ

テストと確認

コンフリクト解決後の重要なステップ:

  • 機能が正常に動作するか確認
  • テストの実行
  • 必要に応じて追加の修正を実施

4. Mainブランチへの統合

すべての確認が完了したら、main branchへの統合を行います:

# mainブランチに切り替え
git checkout main

# featureの変更を取り込む
git merge feature

このアプローチのメリット

  1. リスク管理
    • コンフリクトを早期に発見し、feature branch上で解決できる
    • 問題が発生しても、main branchには影響が及ばない
  2. 品質管理
    • feature branch上で十分なテストが可能
    • 統合前に動作確認を完了できる
  3. スムーズな統合
    • 最終的なmain branchへの統合がスムーズになる
    • 大きな問題が発生するリスクが低減される

まとめ

この方法は、特に以下のような状況で効果を発揮します:

  • 長期間に渡る開発プロジェクト
  • 複数人で同じコードベースを扱う場合
  • クリティカルな機能の開発時

早めにmainの変更を取り込んで確認することで、最終的な統合をより安全に行うことができます。これは、モダンなGit開発フローにおける重要なプラクティスの一つと言えるでしょう。

【Python】バックエンドの理解とフロントエンド開発のポイント

Pythonの実行環境について

Docker環境があれば、Python実行環境を新たにインストールする必要はありません

バックエンドとの連携

確認すべき重要な点:

APIエンドポイントの情報

  • APIのURLとパス
  • 各エンドポイントの機能
  • HTTPメソッド(GET, POST等)
  • 必要なリクエストヘッダー

データ形式

// 例:こういった形でAPIの型定義を作成すると良い
interface ApiResponse {
  status: string;
  data: {
    id: number;
    name: string;
    // その他の項目
  }[];
}
  1. .gitignoreファイルの更新:
Copynode_modules
.env
.env.local
.DS_Store
  1. package.jsonscriptsセクションを確認/追加:
jsonCopy{
  "scripts": {
    "start": "node src/index.js",
    "build": "npm install",
    "vercel-build": "npm run build"
  }
}

デプロイ手順:

  1. GitHubにリポジトリを作成:
bashCopygit init
git add .
git commit -m "Initial commit"
git remote add origin <your-github-repo-url>
git push -u origin main
  1. Vercelでのデプロイ設定:
  • vercel.com にGitHubアカウントでログイン
  • 「New Project」をクリック
  • GitHubリポジトリをインポート
  • プロジェクト設定で環境変数を設定: CopyAWS_ACCESS_KEY_ID=xxxxx AWS_SECRET_ACCESS_KEY=xxxxx AWS_REGION=ap-northeast-1
  • 「Deploy」をクリック

これで自動的にデプロイが開始され、完了すると公開URLが提供されます。その後はGitHubにプッシュするたびに自動デプロイが実行されます。

【WordPress】Atuaテーマ カスタマイズ方法

Atuaテーマ概要

「Atua」というテーマは比較的新しく、インストール数も多くなくあまりメジャーではないといえます。

そのため情報が少なく下記の手段で調べる必要がありそうです

  • テーマの公式ページ
  • 製作者サイト
  • GitHubリポジトリ(もしあれば)でカスタマイズガイドやREADMEファイル

参考に「Lightning」と比較し見ると下記です

AtuaLightning
バージョン1.0.9815.29.0
Active installations600+100,000+
PHP version5.67.4
WordPress version4.76.4

Desert Companion プラグイン

Atuaを有効化するとDesert Companion プラグインのインストール、有効化するようポップアップがでます

有効化するとフロントページにコンテンツが表示されます

フロントページのテンプレートファイルは/page-templates/frontpage.php

Desert Companion プラグインでテンプレートファイルが指定されています(desert-companion\inc\desert-companion-activator.php)

atua/page-templates/frontpage.phpがフロントページでテンプレートファイルとして使用されます

Atua の子テーマ Atus でカスタマイズ

下記ページでテーマファイルはダウンロード可能です

子テーマをインストールするだけで大分デザインが変更されます

Atua 独自で作成した子テーマでカスタマイズ

wp-content/themes/atua-child/を作成

ディレクトリ構成

atua-child
 ┗┳ style.css
  ┣ functions.php
  ┣ 

style.css

/*
Theme Name: atua Child
Theme URI: 
Description: Child theme for atua theme
Author: 
Author URI: 
Template: atua
Version: 1.0.0
License: GNU General Public License v2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Text Domain: atua-child
*/

WordPress管理画面でテーマ追加画面で子テーマとして認識されます

(注意)テーマを有効化するとフロントページのコンテンツが表示されなくなってしまいます。

【問題】子テーマ(atua-child)を有効化すると、Desert Companionプラグインによるフロントページのコンテンツが表示されない

もともと親テーマの「Atua」プラグインの「Desert Companion」でトップページのコンテンツを表示していました

<調査>

デバッグログでDesert_Companion_Atua_frontpageアクションにコールバックが登録されていないことを確認

frontpage.php

<?php
/**
 * Template Name: Frontpage
 */
// ファイルが実行されていることを確認
error_log('Frontpage template is being executed at: ' . date('Y-m-d H:i:s'));

echo '<!-- Child Theme Frontpage Template is loaded -->'; 
get_header();
?>

<div id="content" class="content-area">
    <main id="main" class="site-main">
        <?php
        // 現在のテーマ情報をログに出力
        $current_theme = wp_get_theme();
        error_log('Current theme: ' . $current_theme->get('Name'));
        error_log('Parent theme: ' . $current_theme->parent()->get('Name'));
        // プラグインの初期化状態を確認
        error_log('Checking plugin initialization:');
        error_log('Template Directory: ' . get_template_directory());
        error_log('Stylesheet Directory: ' . get_stylesheet_directory());
        error_log('Theme Root: ' . get_theme_root());

        // デバッグ情報を追加
        global $wp_filter;
        if (isset($wp_filter['Desert_Companion_Atua_frontpage'])) {
            error_log('Registered callbacks for Desert_Companion_Atua_frontpage:');
            error_log(print_r($wp_filter['Desert_Companion_Atua_frontpage'], true));
        } else {
            error_log('No callbacks registered for Desert_Companion_Atua_frontpage');
        }

        // プラグインの読み込み状態を確認
        error_log('Desert Companion Plugin Path: ' . (defined('desert_companion_plugin_dir') ? desert_companion_plugin_dir : 'Not defined'));

        // アクションの実行
        do_action('Desert_Companion_Atua_frontpage');
        ?>
    </main>
</div>

<?php
get_footer();

<原因>

ログの結果frontpage.phpの下記コードは「Desert_Companion_Atua_frontpage」というタイミングで何かを実行しようとしていましたが、実行すべき関数(コールバック)が登録されていない状態でした。

do_action('Desert_Companion_Atua_frontpage');

なぜかというと

  1. プラグインの「Desert Companion」の下記のコードで「Atuaテーマが使われている場合のみ、Atua用の設定ファイルを読み込む」という処理が行われる
  2. 子テーマを使うと、テーマ名がatua-childになるため条件を満たさず設定ファイルが読み込まれない
  3. Desert_Companion_Atua_frontpageにコールバックが登録されない
// desert-companion/desert-companion.phpの中で
if( 'Atua' == $desert_activated_theme->name){
    require desert_companion_plugin_dir . 'inc/themes/atua/atua.php';
}

解決方法:プラグインの条件判定を待たずに、必要なファイルを直接読み込む

子テーマのfunctions.php

<?php
/**
 * Atua Child Theme functions and definitions
 */

/**
 * プラグインの初期化を早い段階で強制的に実行
 */
function force_parent_theme_companion() {
    // プラグインのパスを直接指定して読み込み
    if (defined('desert_companion_plugin_dir')) {
        require_once desert_companion_plugin_dir . 'inc/themes/atua/atua.php';
        // 初期化関数が存在する場合は実行
        if (function_exists('desert_companion_init')) {
            desert_companion_init();
        }
    }
}
// できるだけ早い段階で実行
add_action('init', 'force_parent_theme_companion', 0)

この関数が効果を発揮した理由:

    • プラグインのAtuaテーマ用ファイル(atua.php)を直接読み込んでいる
    • プラグインの初期化関数を明示的に実行している
    • WordPressの初期化段階(init)で早めに実行されるよう設定されている
    WordPressサイト全体を非表示にする一般的な方法

    プラグインでメンテナンス表示

    「Maintenance」プラグイン

    .htaccessファイルで全体にアクセス制限をかける

    1. .htaccess ファイルを編集

    FTPソフトを使ってWordPressのルートディレクトリ(wp-config.phpがある場所)にある .htaccess ファイルを編集します。

    # メンテナンスモード設定開始
    RewriteEngine On
    
    # メンテナンスページ (maintenance.html) へのアクセスは除外
    RewriteCond %{REQUEST_URI} !/maintenance.html$
    
    # 全てのアクセスを maintenance.html にリダイレクト
    RewriteRule ^(.*)$ /maintenance.html [R=302,L]
    
    # メンテナンスモード設定終了

    サブディレクトリ環境での修正方法

    # WordPressが /wordpress/ にインストールされている場合
    RewriteEngine On
    
    # maintenance.html へのアクセスは除外
    RewriteCond %{REQUEST_URI} !^/wordpress/maintenance.html$
    
    # すべてのアクセスを maintenance.html にリダイレクト
    RewriteRule ^(.*)$ /wordpress/maintenance.html [R=302,L]
    

    2. maintenance.html の作成

    WordPressのルートディレクトリに maintenance.html ファイルを作成し、メンテナンス案内ページとしてアップロードします。

    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>メンテナンス中</title>
        <!-- CSSスタイルで簡単なデザイン -->
        <style>
            body {
                text-align: center;
                font-family: Arial, sans-serif;
                background-color: #f4f4f4;
                margin: 0;
                padding: 0;
            }
            .container {
                margin-top: 20%;
            }
            h1 {
                color: #333;
                font-size: 2em;
            }
            p {
                color: #666;
                font-size: 1.2em;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <h1>メンテナンス中</h1>
            <p>現在、サイトはメンテナンス中です。しばらくお待ちください。</p>
        </div>
    </body>
    </html>
    

    WordPressの「設定」で検索エンジンから非表示にする

    1. WordPress管理画面にログイン。
    2. 「設定」→「表示設定」→「検索エンジンでの表示」の項目で「検索エンジンがサイトをインデックスしないようにする」にチェック。
    3. 「変更を保存」ボタンをクリック。

    注意: この方法は「検索エンジン」には非表示にできますが、直接URLにアクセスすれば閲覧可能です。

    【Amazon WorkSpaces】トラブル、対処法

    「WorkSpaceは数秒で準備完了になります…」から変わらない

    • 上記画面で終了してしまう
    • 朝スマホのデザリングで問題発生
    • 夜自宅Wifiですると問題なく接続できる
    • 他のスマホのデザリングでは接続できる
    • 問題のスマホ再起動で接続可能に

    「Amazon WorkSpaces」のウィンドウ内でカーソルが消える

    リモートデスクトップ先のChromeでマウスカーソルが消える
    https://blog.litus.co.jp/2019/07/chrome.html

    WorkSpace でプロファイルが消えてログインできない

    AWS WorkSpace でログインしてもプロファイルが読み込めない現象が発生しました。
    ログインはできますが、次の警告が表示されます。

    アカウントにサインインできません
    この問題は、アカウントからサインアウトし、もう一度サインインすると解決できることがあります。今すぐサインアウトしない場合、作成するファイルや変更の内容はすべて失われます。

    このサインイン不可についてはAWS ワークスペース管理者(インフラ担当者)に対応してもらう必要がありクライアント側では解決はできないようです。

    もしワークスペースからリモートデスクトップにて会社PCに接続する場合は、上記修復後

    AmazonWSにアクセス

    ⬇️

    Windowsアイコン右クリック

    ⬇️

    ファイル名を指定して実行→「mstsc」

    ⬇️

    コンピューター:接続先PC名(CSJxxxx)→接続→ ユーザー名:CSJ\PCログイン名 パスワード:PCログイン時のパスワード

    ⬇️

    オプションの表示」から、「名前をつけて保存」で任意の名前でデスクトップなどに保存可能 →「画面」タブの「リモートセッションですべてのモニターを使用する」にチェックを入れることで、マルチモニター環境も可能。

    VBAの実践的なテクニックを網羅的に解説

    マクロとVBAの概念

    「マクロ」は2つの意味があります

    「マクロ」はExcelを自動処理してくれるもExcelの機能です。

    そして「VBA(Visual Basic for Applications)」という言語で命令書を作成することを「マクロを組む」といいます。

    マクロはどうやって作成するか?

    • 自動で記録(マクロ記録)
    • 自分で書く(VBE(Visual Basic Editor)というツール)…Excelで[Alt] + [F11]

    マクロの記録方法

    再度押すと記録再開されます。

    記録したマクロを使用する方法

    記録したマクロを選択し、「実行」すると記録した処理が実行されます

    「編集」でVBEを起動して編集できます

    VBEを開く手順

    開発タブを表示させる

    「開発」の項目はExcelインストールしたばかりの状態だとだとはチェックが外れいるので有効化しておきます。

    表示された「開発」タブから「Visual Basic Editor」をクリック

    [Alt] + [F11]でも開けます

    マクロのセキュリティレベル

    Excelのセルやワークシートを操作できるだけでなく、悪意のある者が作成すればパソコンに障害を引き起こすような動作もプログラミングすることができます。

    そうした悪意のあるマクロが勝手に実行されないように、セキュリティレベルを設定する機能があります。

    [ファイル] → [オプション] → [トラスト センター]

    既定では1度目のみ警告が表示される

    VBEでプロシージャを実行すると警告が出る

    プローシージャとは

    マクロ動作の基本単位「プロシージャ」には「Sub」と「Function」があります。

    サブ(Sub)とは

    **サブプロシージャ(Sub)**は、「処理手順のまとまり」を表すひとまとまりのコードブロックです。「この手順を実行してね」と指示できる単位と考えると分かりやすいです。Subは処理を実行するための命令文のかたまりであり、戻り値を返しません

    サブの基本形

    Sub サブ名()
        ' ここに実行したい処理を書きます
    End Sub

    処理内容はインデント(字下げ)するのがお作法です

    • Functionで始まりEnd Functionで終わる
    • Function名を処理結果として代入することで戻り値を設定します(関数名 = 値
    • 戻り値は数値でも文字列でもオブジェクトでも、データ型を適宜指定できます
    • Functionはワークシート関数としても使える場合があり、Excelのセルに独自の関数として呼び出せます(ただしパブリック関数として標準モジュールに記述が必要)
    • 別のSubやFunctionから「戻り値 = 関数名(引数)」という形で呼び出して、計算結果などを受け取ることができます
    【Amazon Bedrock】【Node.js】生成AIを使用したVBAフォーマッター

    プロジェクトの内容

    Amazon Bedrockの生成AIを活用した、VBAマクロコードを自動的にフォーマットするWebアプリケーション。インデント、変数名、コメントなどを統一的なルールで整形します。

    機能概要

    • VBAコードのフォーマット
    • インデントの自動調整
    • 変数名の標準化
    • コメントの追加
    • ヘッダー・フッターの挿入

    技術スタック

    • Node.js
    • Express.js
    • HTML/CSS/JavaScript

    必要要件

    • Node.js (v14以上)
    • npm (v6以上)

    セットアップ手順

    1. プロジェクト構成ファイルを作成
    2. 依存パッケージのインストール
    npm install
    1. 開発サーバーの起動
    npm run dev
    1. ブラウザでアクセス
    http://localhost:3000

    プロジェクト構成

    vba-formatter/
    ├── src/
    │   ├── index.js           # メインサーバーファイル
    │   ├── formatter.js       # 基本的なフォーマット処理
    │   ├── bedrock/          # Bedrock関連の新規ディレクトリ
    │   │   ├── client.js     # Bedrock クライアント設定
    │   │   ├── prompts.js    # プロンプトテンプレート
    │   │   └── analyzer.js   # コード分析ロジック
    │   └── public/           # フロントエンド

    Bedorock関連

    .env

    AWS_ACCESS_KEY_ID=あなたのアクセスキー
    AWS_SECRET_ACCESS_KEY=あなたのシークレットキー
    AWS_REGION=us-east-1

    dotenvは、.envファイルを読み込んで、その内容を環境変数としてprocess.envに設定するためのパッケージです。

    npm install dotenv

    @aws-sdk/client-bedrock-runtime: Amazon Bedrockと通信するため下記インストール

    npm install dotenv @aws-sdk/client-bedrock-runtime

    client.js:Amazon Bedrockへの接続設定

    require('dotenv').config();
    const { BedrockRuntimeClient } = require("@aws-sdk/client-bedrock-runtime");
    
    // Bedrockクライアントの設定
    const client = new BedrockRuntimeClient({ 
        region: process.env.AWS_REGION || "us-east-1",
        credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
        }
    });
    
    module.exports = client;

    prompts.js:AIへの指示(プロンプト)テンプレート

    /**
    * prompts.js
    * AIへの指示内容を定義するファイル
    * VBAコードの分析と改善の方法をAIに指示します
    */
    
    // AIへの指示内容を定義(バッククォートで囲むことで複数行の文字列を作成)
    const ANALYSIS_PROMPT = `
    # ここからがAIへの指示内容です
    =============================================
    
    # AIの役割定義
    あなたはVBAコードの専門家として、以下のコードを分析し、優先順位に従って改善してください。
    
    # 入力コード部分
    \`\`\`vba
    {code}
    \`\`\`
    
    # 改善要件(優先順位:高 - 第一弾)
    =============================================
    1. インデント処理
      - SUBからENDSUBまでTABひとつ分のインデント
      - IF文、With文、For文等の制御構造にもインデント
      - ネストレベルに応じて適切なインデント
    
    2. ヘッダー・フッター追加
      - Sub開始時のヘッダー:
        Option Explicit
        '***************************************************
        ' [プロシージャ名]
        '***************************************************
        '【機  能】[機能の説明]
        '【引  数】[引数の説明]
        '【戻 り 値】[戻り値の説明]
        '【機能説明】[詳細な説明]
        '【備  考】[その他特記事項]
        ' Copyright(c) 2024 Your Company All Rights Reserved.
        '***************************************************
        
      - 処理の区切り:
        '***************************************************
        ' 変数宣言
        '***************************************************
        [変数宣言部分]
    
      - Sub終了時のフッター:
        '***************************************************
        '---------------------------------------------------
        ' Version
        '---------------------------------------------------
        ' 1.00 | 2024.01.01 | ********* | *********
        '***************************************************
    
    # 改善要件(優先順位:中 - 第二弾)
    =============================================
    3. 変数名の改善
      - 一文字の変数を禁止
      - セル参照の変数は意味のある名前に変更
        (例: Last_Row, Last_Column)
      - 配列はArrayを付ける
      - その他の変数はDataを付ける
    
    4. Public変数・Call文の処理
      - Public変数にはPublic関数であることをコメントで記載
      - Call文にはモジュール名を追加
        (例: Call Module1.印刷)
    
    5. 変数宣言のコメント
      - 宣言した変数の横にコメントを追加
      - 使用目的や内容を簡潔に説明
    
    # 改善要件(優先順位:中 - 第三弾)
    =============================================
    6. 変数・シート名の処理
      - 日本語変数名を英語に変更
      - 未宣言変数を「変数宣言」セクションに追加
      - 型判定できない変数はVariantに
      - シート名は定数(Const)で定義
      - シート追加時の名前はVariantで処理
    
    7. コメント追加
      - IF文、With文などの制御構造にコメント
      - 配列の内容説明
      - SET文の説明
      - Offsetのコメント必須
        (参照セルと目的を明記)
    
    8. コードの最適化
      - 類似コードが3回以上続く場合はループ化
      - パスの直書きは避け、pathに置換
        (元のパスはコメントとして保持)
    
    # 出力形式
    =============================================
    優先順位の高い要件(第一弾)から順に適用し、
    改善したVBAコードのみを出力してください。
    `;
    
    module.exports = {
       ANALYSIS_PROMPT
    };

    analyzer.js:Bedrockとの通信処理

    // AWS SDKからBedrockのクライアントとコマンドをインポート
    const { InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime");
    const client = require('./client');
    const { ANALYSIS_PROMPT } = require('./prompts');
    
    /**
     * VBAコードをClaudeモデルで分析する関数
     * @param {string} vbaCode - 分析対象のVBAコード
     * @returns {Promise<string>} - 改善されたVBAコード
     */
    async function analyzeCode(vbaCode) {
        try {
            // プロンプトテンプレートにVBAコードを挿入
            const prompt = ANALYSIS_PROMPT.replace('{code}', vbaCode);
    
            // Claude用のリクエストコマンドを作成
            const command = new InvokeModelCommand({
                // Claude 3 Sonnetモデルを指定
                modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
                contentType: "application/json",
                accept: "application/json",
                // Claude用のリクエスト形式
                body: JSON.stringify({
                    anthropic_version: "bedrock-2023-05-31",
                    max_tokens: 2048,
                    messages: [
                        {
                            role: "user",
                            content: prompt
                        }
                    ]
                })
            });
    
            // BedrockAPIを呼び出し
            const response = await client.send(command);
            
            // レスポンスの処理(Claude形式)
            const responseBody = Buffer.from(response.body).toString('utf8');
            const jsonResponse = JSON.parse(responseBody);
    
            // Claudeのレスポンス形式から結果を抽出
            if (jsonResponse.content && jsonResponse.content[0]) {
                return jsonResponse.content[0].text;
            }
    
            throw new Error('AIモデルからの応答が不正な形式です');
        } catch (error) {
            console.error("コード分析エラー:", error);
            throw error;
        }
    }
    
    module.exports = {
        analyzeCode
    };

    メインファイル

    src/index.js:Webサーバーのメイン処理

    // ========================================
    // サーバーのメインファイル (index.js)
    // VBAフォーマッターのバックエンド処理を担当
    // ========================================
    
    // 必要なモジュールのインポート
    const express = require('express');
    const { analyzeCode } = require('./bedrock/analyzer');  // Bedrock AIの分析機能
    const { formatVBA } = require('./formatter'); // 基本的なフォーマット機能
    const { ANALYSIS_PROMPT } = require('./bedrock/prompts'); // AIプロンプトの取得
    
    // Expressアプリケーションの初期化
    const app = express();
    
    // ミドルウェアの設定
    // 静的ファイル(HTML/CSS/JS)の提供設定
    app.use(express.static('src/public')); 
    // JSONリクエストの解析を有効化
    app.use(express.json());
    
    /**
     * AIへのプロンプト内容を提供するエンドポイント
     * フロントエンドでAIへの指示内容を表示するために使用
     */
    app.get('/prompts', (req, res) => {
        res.json({ 
            success: true, 
            prompt: ANALYSIS_PROMPT 
        });
    });
    
    /**
     * VBAコード分析API
     * フロントエンドから送信されたコードを分析・改善
     */
    app.post('/format', async (req, res) => {
        try {
            // リクエストからコードを取得
            const { code } = req.body;
            
            // 入力チェック
            if (!code) {
                return res.status(400).json({ 
                    success: false, 
                    error: 'コードが提供されていません' 
                });
            }
    
            // 1. 基本的なフォーマットを適用
            const basicFormatted = formatVBA(code);
    
            // 2. AIによる高度な分析と改善
            const aiAnalysis = await analyzeCode(basicFormatted);
            
            // 成功時のレスポンス
            res.json({ 
                success: true, 
                formatted: aiAnalysis
            });
    
        } catch (error) {
            // エラー発生時の処理
            console.error('エラーが発生しました:', error);
            res.status(500).json({ 
                success: false, 
                error: error.message 
            });
        }
    });
    
    // サーバーの起動設定
    const PORT = process.env.PORT || 3000;
    
    // 全てのIPアドレスからのアクセスを許可(社内ネットワーク用)
    app.listen(PORT, '0.0.0.0', () => {
        console.log('========================================');
        console.log(`VBA Formatter サーバー起動`);
        console.log(`http://localhost:${PORT}`);
        console.log('========================================');
    });

    フロントエンド

    public/index.html:ユーザーインターフェース

    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <!-- 文字エンコーディングとビューポートの設定 -->
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>VBA Formatter</title>
        <!-- reset.css ress -->
        <link rel="stylesheet" href="https://unpkg.com/ress/dist/ress.min.css" />
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link rel="stylesheet" href="style.css">
        <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
        <!-- スタイルシートの読み込み -->
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <!-- メインコンテナ -->
        <div class="container">
            <h1>VBA Formatter</h1>
    
            <!-- AIへの指示内容表示セクション(src/bedrock/prompts.jsの内容) -->
            <div class="prompt-section">
                <h3>AIへの指示内容</h3>
                <!-- プロンプト内容を動的に表示する要素 -->
                <div class="prompt-display-container">
                    <div class="user-icon"></div>
                    <div class="prompt-display-wrapper">
                        <pre class="prompt-display">ユーザーからの指示内容がここに表示されます</pre>
                    </div>
                </div>
            </div>
    
            <!-- コードエディタ部分 -->
            <div class="editors">
                <!-- 入力用エディタ -->
                <div class="editor">
                    <h3>分析するコード</h3>
                    <!-- 入力用テキストエリア:ユーザーがコードを入力または貼り付け可能 -->
                    <textarea id="input" placeholder="VBAコードを入力してください"></textarea>
                    
                    <!-- ファイル選択ボタン:.basと.txtファイルのみ受付 -->
                    <input type="file" id="vbaFile" accept=".bas,.txt">
                    <!-- <span class="detail">※.basと.txtファイルのみ受付</span> -->
                </div>
                <!-- 出力用エディタ -->
                <div class="editor">
                    <h3>改善後のコード</h3>
                    <!-- 出力用テキストエリア:読み取り専用で改善されたコードを表示 -->
                    <textarea id="output" readonly></textarea>
                    
                    <!-- フォーマット実行ボタン -->
                    <button onclick="formatCode()">Format</button>
                </div>
            </div>
        </div>
    
        <!-- JavaScriptの処理 -->
        <script>
            // ページ読み込み時にプロンプト内容を取得
            window.onload = async function() {
                try {
                    const response = await fetch('/prompts');
                    const data = await response.json();
                    if (data.success) {
                        document.querySelector('.prompt-display').textContent = data.prompt;
                    }
                } catch (error) {
                    console.error('プロンプト取得エラー:', error);
                    document.querySelector('.prompt-display').textContent = 'プロンプトの読み込みに失敗しました';
                }
            };
    
            /**
             * コードのフォーマットを実行する関数
             * 1. 入力を取得
             * 2. サーバーにリクエスト
             * 3. 結果を表示
             */
            async function formatCode() {
                // テキストエリアから入力を取得
                const input = document.getElementById('input').value;
                // ステータス表示用の要素取得
                const formatButton = document.querySelector('button');
                
                // 入力値のバリデーション
                if (!input) {
                    alert('VBAコードを入力してください');
                    return;
                }
    
                // 処理開始のステータス表示
                formatButton.innerHTML = '<span class="processing"><span></span><span></span><span></span></span>';
    
                try {
                    // サーバーへのリクエスト
                    const response = await fetch('/format', {
                        method: 'POST',  // POSTメソッドを使用
                        headers: {
                            'Content-Type': 'application/json'  // JSONデータとして送信
                        },
                        body: JSON.stringify({ code: input })  // 入力をJSONに変換
                    });
    
                    // レスポンスの処理
                    const data = await response.json();
                    
                    // 成功時は結果を表示、失敗時はエラーメッセージを表示
                    if (data.success) {
                        document.getElementById('output').value = data.formatted;
                        formatButton.textContent = '分析完了!';
                    } else {
                        formatButton.textContent = 'エラー: ' + data.error;
                    }
                } catch (error) {
                    // 通信エラーなどの例外処理
                    formatButton.textContent = 'エラー: ' + error.message;
                }
                formatButton.classList.remove('processing'); // アニメーションクラスを削除
            }
    
            /**
             * ファイル選択時の処理
             * 選択されたファイルの内容を入力エリアに表示
             */
            document.getElementById('vbaFile').addEventListener('change', (e) => {
                // 選択されたファイルを取得
                const file = e.target.files[0];
                if (file) {
                    // FileReaderを使用してファイルの内容を読み込み
                    const reader = new FileReader();
                    // ファイル読み込み完了時の処理
                    reader.onload = (e) => {
                        // 読み込んだ内容を入力エリアに設定
                        document.getElementById('input').value = e.target.result;
                    };
                    // ファイルをテキストとして読み込み開始
                    reader.readAsText(file);
                }
            });
        </script>
    </body>
    </html>

    public/style.css:見た目の装飾

    body {
        font-family: "Noto Sans JP", serif;
          background-color: #f1f2f3;
      }
      /* メインコンテナ */
      .container {
        max-width: 1200px;
        margin: 0 auto;
        padding: 0 2rem 2rem;
      }
      
      h1 {
          background-color: #fff;
          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
          text-align: center;
          letter-spacing: 0.1em;
          padding: 1rem 0;
          margin: 0 calc(50% - 50vw);
          width: 100vw;
          margin-bottom: 2rem;
      }
      
      h3 {
        margin-bottom: 1rem;
          text-align: center;
          letter-spacing: .1em;
      }
      
      /* AIへの指示内容表示部分 */
      .prompt-section {
        margin-bottom: 2rem;
      }
      
      .prompt-display-container {
          display: flex;
        align-items: end;
          justify-content: center;
        gap: 2rem;
          margin: 0 auto;
      }
      
      .user-icon {
        width: 40px;
        height: 40px;
        background-color: #f6ce55;
        border-radius: 50%;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
          margin-bottom: 3rem;
          position: relative;
      }
      
      .user-icon::after {
        content: "";
        position: absolute;
        top: 96%;
        left: 50%;
        transform: translateX(-50%);
        width: 50px;
        height: 40px;
        background-color: #f6ce55;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        border-top-left-radius: 50%;
        border-top-right-radius: 50%;
      }
      
      .prompt-display-wrapper {
          max-width: 80%;
          display: flex;
          align-items: center;
          justify-content: center;
          background-color: #fff;
        border-radius: 5rem 5rem 5rem 0;
        box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
          padding: 2rem;
      }
      
      .prompt-display {
        font-family: monospace;
        font-size: 0.825rem;
        line-height: 1.5;
        white-space: pre-wrap;
          height: 300px;
          overflow-y: scroll;
          resize: vertical;
      }
      
      /* カスタムスクロールバーのスタイル */
      .prompt-display::-webkit-scrollbar {
        width: 0.5rem; /* スクロールバーの幅 */
      }
      
      .prompt-display::-webkit-scrollbar-thumb {
        background: #888;
        border-radius: 1rem;
      }
      
      /* エディタ部分 */
      .editors {
        display: flex;
        gap: 2rem;
          margin-bottom: 1rem;
      }
      
      .editor {
        flex: 1;
          display: flex;
          flex-direction: column;
      }
      
      .editor > * {
          margin-bottom: 1rem;
      }
      
      .editor > *:last-child {
          margin-top: auto!important;
          }
      
      .editor input {
          height: 3rem;
          background-color: #ccc;
        padding: 0.5rem 1rem;
        border-radius: 4px;
          width: 100%;
          transition: all 0.3s ease;
      }
      
      .editor input:hover {
          background-color: #bbb;
      }
      
      .editor .detail {
          margin-top: 0.5rem;
          color: #FF565D;
      }
      
      /* テキストエリア */
      .editor textarea {
        width: 100%;
        height: 500px;
        font-family: monospace;
        padding: 10px;
        font-size: 14px;
        line-height: 1.5;
        border: 2px solid #ccc;
          background-color: #fff;
        border-radius: 4px;
      }
      
      button {
          height: 3rem;
        padding: 0.5rem 1rem;
        background-color: #e9be3b;
          ;
        border-radius: 4px;
        color: white;
          letter-spacing: 0.1em;
        border: none;
        cursor: pointer;
      }
      
      button:hover {
        background-color: #d3b458;
          ;
          transition: all 0.3s ease;
      }
      
      /* アニメーションを追加するクラス */
      .processing {
        display: inline-flex;
        gap: 0.5rem;
      }
      
      .processing span {
        display: inline-block;
        width: 0.75rem;
        height: 0.75rem;
        background-color: #fff;
        border-radius: 50%;
        animation: fadeDots 1.5s infinite ease-in-out;
      }
      
      /* それぞれのドットに異なるアニメーションの遅延を設定 */
      .processing span:nth-child(1) {
        animation-delay: 0s;
      }
      
      .processing span:nth-child(2) {
        animation-delay: 0.2s;
      }
      
      .processing span:nth-child(3) {
        animation-delay: 0.4s;
      }
      
      /* 薄さ(透明度)を変化させるアニメーション */
      @keyframes fadeDots {
        0%, 100% { opacity: 0.2; } /* 薄くなる */
        50% { opacity: 1; } /* 濃くなる */
      }
      
      

    その他

    formatter.js:VBAコードの基本的な整形処理

    // VBAコードをフォーマットする関数
    // 引数:code - フォーマットする元のVBAコードの文字列
    // 戻り値:フォーマット済みのVBAコードの文字列
    function formatVBA(code) {
        // ステップ1: コードを行単位に分割
        // splitは文字列を指定した区切り文字(ここでは改行)で配列に分割する
        const lines = code.split('\n');
        
        // ステップ2: インデントレベルを管理する変数を初期化
        // インデントレベルは、コードのネストの深さを表す
        // 例:Sub内なら1、If文の中なら2、というように増えていく
        let indentLevel = 0;
        
        // ステップ3: フォーマット済みの行を格納する配列を準備
        const formattedLines = [];
    
        // ステップ4: キーワードを定義
        // これらの単語が行の先頭にあるかどうかでインデントを判断する
        const startKeywords = ['sub', 'function', 'if', 'for', 'do'];
        const endKeywords = ['end sub', 'end function', 'end if', 'next', 'loop'];
    
        // ステップ5: 各行を処理
        // forEachは配列の各要素に対して処理を行う
        lines.forEach((line, lineNumber) => {
            // 行の空白を削除して、小文字に変換(比較用)
            const trimmedLine = line.trim();
            const lowerLine = trimmedLine.toLowerCase();
    
            // ステップ6: 空行の処理
            if (trimmedLine === '') {
                formattedLines.push('');
                return; // 次の行の処理へ
            }
    
            // ステップ7: インデントレベルの調整
            // 7.1: ブロックの終了を示すキーワードの前にインデントを減らす
            if (endKeywords.some(keyword => lowerLine.startsWith(keyword))) {
                indentLevel = Math.max(0, indentLevel - 1);
            }
    
            // ステップ8: 行のフォーマット
            // 8.1: 現在のインデントレベルに応じてスペースを追加
            // repeat()は文字列を指定回数繰り返す
            const indent = '    '.repeat(indentLevel);
            
            // 8.2: コメントの処理
            // シングルクォートで始まる行はコメント
            if (trimmedLine.startsWith("'")) {
                formattedLines.push(indent + trimmedLine);
            } else {
                // 8.3: 通常のコードの処理
                formattedLines.push(indent + trimmedLine);
            }
    
            // ステップ9: 次の行のためのインデントレベルの更新
            // 9.1: ブロックの開始を示すキーワードの後でインデントを増やす
            if (startKeywords.some(keyword => lowerLine.startsWith(keyword))) {
                indentLevel++;
            }
        });
    
        // ステップ10: フォーマット済みの行を改行文字で結合して1つの文字列に戻す
        return formattedLines.join('\n');
    }
    
    // ステップ11: ヘッダーコメントを生成する関数
    function createHeader() {
        return [
            "Option Explicit",
            "'***************************************************",
            "' VBAプログラム",
            "'***************************************************",
            "'【機  能】",
            "'【返 り 値】なし",
            "'【機能説明】",
            "'【備  考】",
            "' Copyright(c) " + new Date().getFullYear(),
            "'***************************************************",
            ""
        ].join('\n');
    }
    
    // ステップ12: 変数をチェックして適切な型を提案する関数
    function suggestVariableType(variableName) {
        // 変数名から型を推測
        if (variableName.toLowerCase().includes('count')) {
            return 'Long';
        } else if (variableName.toLowerCase().includes('date')) {
            return 'Date';
        } else if (variableName.toLowerCase().includes('flag')) {
            return 'Boolean';
        } else if (variableName.toLowerCase().includes('name')) {
            return 'String';
        } else {
            return 'Variant';
        }
    }
    
    // ステップ13: モジュールの外部公開
    // このファイルの関数を他のファイルから使用できるようにする
    module.exports = {
        formatVBA,
        createHeader,
        suggestVariableType
    };

    プロジェクト設定ファイル

    package.json:依存パッケージの管理

    {
        "name": "vba-formatter",
        "version": "1.0.0",
        "description": "VBA Code Formatter",
        "main": "src/index.js",
        "scripts": {
            "start": "node src/index.js",
            "dev": "nodemon src/index.js"
        },
        "dependencies": {
            "@aws-sdk/client-bedrock-runtime": "^3.712.0",
            "dotenv": "^16.4.7",
            "express": "^4.18.2",
            "multer": "^1.4.5-lts.1"
        },
        "devDependencies": {
            "nodemon": "^3.0.2"
        }
    }
    

    test-connection.js:正しく動作するかテスト

    require('dotenv').config();
    const { BedrockRuntimeClient, InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime");
    
    async function testBedrock() {
        console.log("環境変数確認:");
        console.log("Region:", process.env.AWS_REGION);
        console.log("Access Key ID exists:", !!process.env.AWS_ACCESS_KEY_ID);
        console.log("Secret Access Key exists:", !!process.env.AWS_SECRET_ACCESS_KEY);
    
        const client = new BedrockRuntimeClient({
            region: process.env.AWS_REGION || "us-east-1",
            credentials: {
                accessKeyId: process.env.AWS_ACCESS_KEY_ID,
                secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
            }
        });
    
        // モデルごとに適切なリクエスト本文を生成する関数
        function getRequestBody(modelId) {
            if (modelId.startsWith('amazon.titan')) {
                return {
                    inputText: "Hello",
                    textGenerationConfig: {
                        maxTokenCount: 10,
                        temperature: 0.7,
                        topP: 1
                    }
                };
            } else if (modelId.startsWith('anthropic')) {
                return {
                    anthropic_version: "bedrock-2023-05-31",
                    max_tokens: 1024,
                    messages: [
                        {
                            role: "user",
                            content: "Hello, Claude!"
                        }
                    ]
                };
            }
        }
    
        // テストするモデルのリスト
        const modelIds = [
            // Titan Models(動作確認済み)
            "amazon.titan-text-express-v1",
            "amazon.titan-text-lite-v1",
    
            // Mistral Models
            "mistral.mistral-7b-instruct-v0:2",
            "mistral.mistral-large-2402-v1:0",
            "mistral.mistral-large-2407-v1:0",
            "mistral.mistral-small-2402-v1:0",
            "mistral.mixtral-8x7b-instruct-v0:1",
    
            // Claude Models
            "anthropic.claude-3-sonnet-20240229-v1:0",  // Claude 3 Sonnet
            "anthropic.claude-3-haiku-20240307-v1:0",   // Claude 3 Haiku
            "anthropic.claude-3-opus-20240229-v1:0",    // Claude 3 Opus
            "anthropic.claude-3-5-haiku-20241022-v1:0", // Claude 3.5 Haiku
            "anthropic.claude-3-5-sonnet-20241022-v2:0",// Claude 3.5 Sonnet v2
            "anthropic.claude-3-5-sonnet-20240620-v1:0",// Claude 3.5 Sonnet
            "anthropic.claude-v2:1",                    // Claude 2.1
            "anthropic.claude-v2",                      // Claude 2
            "anthropic.claude-instant-v1"               // Claude Instant
        ];
    
        // 各モデルをテスト
        for (const modelId of modelIds) {
            console.log(`\nテスト: ${modelId}`);
            try {
                const command = new InvokeModelCommand({
                    modelId: modelId,
                    contentType: "application/json",
                    accept: "application/json",
                    body: JSON.stringify(getRequestBody(modelId))
                });
                const response = await client.send(command);
                console.log(`✅ ${modelId} は利用可能です`);
                
                // レスポンスの内容を表示
                const responseBody = Buffer.from(response.body).toString('utf8');
                console.log('応答:', responseBody);
            } catch (error) {
                console.log(`❌ ${modelId}: ${error.message}`);
            }
        }
    }
    
    testBedrock();
    node test-connection.js
    https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html

    使用方法

    1. Webブラウザで http://localhost:3000 にアクセス
    2. テキストエリアにVBAコードを貼り付け、または.basファイルを選択
    3. 「Format」ボタンをクリック
    4. 右側のテキストエリアにフォーマット済みコードが表示

    要件定義

    ### VBA Formatter with Amazon Bedrock プロジェクト説明
    
    #### プロジェクト概要
    このプロジェクトはJavaScript言語でNode.jsを利用し、ExpressというWebアプリケーションフレームワークで作成されています。VBAコードを改善するためにAmazon BedrockのAIを活用しています。
    
    #### ファイル構成と役割
    1. `src/public/index.html`
       - Webブラウザで表示されるユーザーインターフェース
       - VBAコードの入力とAI改善結果の表示を担当
    
    2. `src/index.js`
       - サーバープログラムのメインファイル
       - ブラウザからのリクエストを処理
       - AIとの連携を管理
    
    3. `src/bedrock/prompts.js`
       - AIへの指示内容(プロンプト)を定義
       - コードの改善ルールを記述
    
    4. `src/bedrock/analyzer.js`
       - Amazon BedrockのAIと通信
       - AIからの応答を処理
    
    #### 処理の流れ
    1. ブラウザでVBAコードを入力
    2. サーバーがコードを受け取り
    3. AIに分析を依頼
    4. 改善されたコードが表示
    
    #### 改善点
    これまでAWSコンソールのAmazon Bedrockプレイグラウンドでチャット形式で行っていたAIとの対話を、使いやすいWebインターフェースで実現。以下が改善されました:
    - コードの入力が簡単に
    - AIへの指示内容が明確に表示
    - 結果の表示が見やすく
    - 複数のコードを連続して処理可能
    
    #### 拡張性
    - AIへの指示内容(プロンプト)を簡単に更新可能
    - 新しい改善ルールの追加が容易
    - チーム固有のコーディングルールも組み込み可能
    
    #### 今後の展開
    - より詳細なコード分析機能の追加
    - チーム固有のルールの組み込み
    - バッチ処理機能の追加

    要確認

    formatter.jsで生成A使用しない処理を使用できていない

    改善項目

    AWS SDK の追加活用

    • CloudWatch Logs: AIの応答ログを収集・分析
    • S3: VBAコードのバージョン管理や履歴保存
    • DynamoDB: 変換履歴や頻出パターンの保存

    UI

    • 差分表示機能(基本的なnpmパッケージで実現可能)

    vercelで公開

    ExpressがNext.jsではないのにスムーズな理由は:

    • Vercelは「Next.jsのためのプラットフォーム」ではなく
    • 「モダンなWebアプリケーションのためのプラットフォーム」
    • ExpressもNode.jsアプリケーションとして自然にサポート
    • @vercel/nodeが複雑な変換を全て自動化

    このため、ExpressアプリケーションでもNext.jsと同じようにスムーズにデプロイできるのです。

    1. @vercel/node
    ExpressのVercelデプロイ 1. vercel.json追加 @vercel/node で Express を自動変換 2. GitHubにプッシュ コードをGitHubにアップロード 3. 環境変数設定 Vercel管理画面で環境変数を設定 Vercelが自動で処理: SSL証明書・ドメイン割り当て・デプロイ

    デプロイフロー

    vercel.jsonの作成

    vercel.jsonは、Vercelにデプロイする際の設定ファイルです。

    vercel.json

    {
      "version": 2,
      "builds": [
        {
          "src": "src/index.js",
          "use": "@vercel/node"
        },
        {
          "src": "src/public/**",
          "use": "@vercel/static"
        }
      ],
      "routes": [
        {
          "src": "/prompts",
          "dest": "/src/index.js"
        },
        {
          "src": "/format",
          "dest": "/src/index.js"
        },
        {
          "src": "/api/(.*)",
          "dest": "/src/index.js"
        },
        {
          "src": "/(.+\\.[a-zA-Z]+)$",
          "dest": "/src/public/$1"
        },
        {
          "src": "/(.*)",
          "dest": "/src/public/index.html"
        }
      ]
    }

    .gitignore

    node_modules
    .env
    .env.local
    .DS_Store

    GitHubにリポジトリを作成

    vercel.com にGitHubアカウントでログイン

    GitHubリポジトリをインポート

    .envの内容を「Environment Variables」設定

    Deployで公開完了

    最新アップデート版

    リフォーマット版

    お名前.comでFTPソフトを使用する手順

    お名前ドットコム管理画面

    レンタルサーバーのコントロールパネルに入ります

    「基本設定」→「ファイル管理」クリック

    該当のドメインを選択→「はじめる」

    設定情報で確認できる下記がFTPソフトで接続する際に必要です

    • ユーザー名
    • パスワード
    • FTPソフト

    FTPソフトでの接続

    SFTPで接続できない場合FTPでする必要があります

    「FTPサーバー」が「ホスト名」に該当します

    FTPソフト設定画面のサンプル ※ 重要: SFTPではなく、FTPを使用してください。 ※ お名前.com画面の「FTPサーバー」の値を  「ホスト名」に入力してください。 FTP設定 転送プロトコル: FTP ▼ ホスト名: www1234.onamae.ne.jp ポート: 21 ユーザー名: user@example.onamae.ne.jp パスワード: •••••••• 接続先 ディレクトリ: /home/rXXXXXXX/public_html/ 接続
    PHPのサーバー変数を確認する方法

    Webサーバー経由とターミナル実行時の$_SERVER変数の違い

    Webサーバー経由での実行で確認(アクセス)することが重要です

    Webサーバー経由でアクセスすることで$_SERVERにはHTTPリクエストの情報が含まれます

    $_SERVER変数の実行環境による違い php -a $_SERVER = [ ‘HOSTNAME’, ‘HOME’, ‘TERM’, // HTTPデータなし ]; ターミナル実行 $_SERVER = [ ‘HTTP_HOST’, ‘REQUEST_METHOD’, ‘REMOTE_ADDR’, ‘HTTP_USER_AGENT’, // HTTPデータあり ]; Webサーバー経由実行 Webサーバー経由の実行では、HTTPリクエストに関連する追加情報が$_SERVER変数に含まれます

    Webサーバー経由でアクセスする手順

    1. プロジェクト内にPHPファイルを作成
    2. ブラウザからアクセス

    Docker Desktopで開発している場合

    対象のコンテナを選択

    「Exec」タブから、順番に以下のコマンドを実行

    パーミッションの確認と必要に応じて変更

    ls -l /var/www/html/server-info.php
    chmod 644 /var/www/html/server-info.php

    PHPファイルの作成

    echo '<?php
    header("Content-Type: application/json");
    echo json_encode($_SERVER);' > /var/www/html/server-info.php

    Webサーバー経由でアクセス

    curl localhost/server-info.php

    <注意点>

    パスが/var/www/html/でない場合は、プロジェクトの正しいパスに変更してください

    Next.jsのSSGサイトにGoogleアドセンスを設置する手順

    Next.jsのSSGサイトへのGoogle AdSenseの設置方法

    自動広告

    自動広告とは

    1つのコードを設置するだけで、AIが自動的にコンテンツを分析し、最適な位置に広告を配置する仕組み

    自動広告の配置イメージ 青色部分:AIが自動で広告を配置
    import './globals.css'
    import Script from 'next/script';
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode
    }) {
      return (
        <html lang="en">
          <head>
            <Script
              async
              src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
              data-ad-client="xxxxx"
              strategy="afterInteractive"
              crossOrigin="anonymous"
            />
          </head>
          <body><main className="container">
              {children}
            </main></body>
        </html>
      );
    }
    Next.js SSG レスポンシブでレンダリングのちらつき

    https://www.gaji.jp/blog/2023/10/12/17283

    【GTM】カスタムイベントをトリガーにして確実にCV計測

    カスタムイベントとは:

    • データレイヤーを通じてGTMに送信される独自のイベントです
    • 通常のページビューやクリックなどの標準イベントとは異なり、開発者が任意のタイミングで発火できます
    • JavaScriptを使って明示的にトリガーする必要があります

    カスタムイベントの設定方法:

    1. Webサイトのコード側で、以下のようなDataLayer.pushを実装します:
    dataLayer.push({
      'event': 'customEventName',  // イベント名
      'eventCategory': 'category', // 任意のパラメータ
      'eventAction': 'action',     // 任意のパラメータ
      // その他必要なデータ
    });
    1. GTM側では:
    • トリガーの新規作成で「カスタムイベント」を選択
    • イベント名に、dataLayer.pushで指定した’event’の値(例:’customEventName’)を設定

    発火のタイミング:

    • フォーム送信完了時
    • 動画の再生開始/終了時
    • スクロール到達時
    • モーダル表示時 など、サイトの要件に応じて任意のタイミングで発火させることができます

    具体例:サンクスページでの計測(よくある課題)

    フォーム送信 → サンクスページ遷移 → 計測

    よくある課題

    このような通常のフローだと、以下のリスクがあります:

    • ページ遷移の途中でユーザーが離脱してしまう
    • 通信エラーでサンクスページが表示されない
    • ブラウザバックなどで誤カウント
    • サンクスページが正しく読み込まれない

    計測のタイミングを前倒し

    コンバージョン計測方式の比較 従来の方式(リスクあり) 開始 フォーム送信 サンクス遷移 計測 ❌ 遷移失敗のリスク virtual_pageview方式(推奨) 開始 フォーム送信 計測 サンクス遷移 ✓ 確実な計測が可能
    • フォーム送信完了時に仮想的なページビューイベントを発火
    • 実際のページ遷移ではなく、JavaScriptによって発火される

    Google TagManagerの推奨プラクティスの1つより確実にコンバージョンを計測するための、一般的で信頼性の高い方法だと言えます。多くのウェブサイトで採用されている標準的なアプローチです。

    フォーム送信 → 【ここで計測】 → サンクスページ遷移
    dataLayer.push({
      'event': 'virtual_pageview',
      // 必要に応じて追加パラメータ
    });

    トリガーにカスタムイベントを設定

    <トリガーの設定>

    トリガーのタイプカスタムイベント
    イベント名virtual_pageview(✓正規表現一致を使用)
    このトリガーの発生場所すべてのカスタムイベント

      注意点

        • 各広告媒体専用のCVタグを個別に紐付ける必要がある
        • 例えば、LINE広告のCV計測をする場合は、LINE用のCVタグを別途このトリガーに紐付ける必要がある
        • タグが紐付けられていない媒体は計測されない
        広告運用とCV計測の全体フロー 広告媒体 Google広告 LINE広告 Yahoo!広告 ランディングページ フォーム 申し込み Google Tag Manager virtual_pageview トリガー 紐付けられるコンバージョンタグ Google広告 LINE広告 Yahoo!広告 広告クリック フォーム送信時に発火

        本日のCV計測に関するMTGについて、参加をご相談させていただきたく存じます。
        現在、以下の点について理解を深めたいと考えております: ・各広告媒体でのCV計測の仕組み ・各LPでのコンバージョン実装方法 ・virtual_pageviewの具体的な運用方法 まだ理解が不十分な部分もあり、MTGを通じて学ばせていただければと存じます。

        Amazon Bedrockの開発環境構築とAPI活用ガイド

        はじめに

        Amazon Bedrockの活用方法は、コンソールのプレイグラウンドだけではありません。開発環境を構築し、外部APIと連携することで、より高度な機能の実装が可能になります。

        従来のAmazon Bedrockのコンソール画面からの基本的な操作

        AWSマネジメントコンソールにログイン

        「Amazon Bedrock」を選択

        「Model access」を選択 使用したいモデルの横にあるチェックボックスを選択

        左側メニューから「Playground」を選択

        設定 1. Temperature(回答の硬さ・柔らかさ) 0.0 硬め 0.5 標準 1.0 柔らかめ 硬め:ビジネス文書・校閲 標準:一般的な会話 柔らかめ:創作・アイデア出し 2. Top-P(回答の選択肢の幅) 0.1 狭い 0.5 中間 0.9 広い 狭い:限定的な選択肢から選ぶ 中間:バランスの取れた選択 広い:多様な選択肢から選ぶ(推奨)

        開発環境の選択肢

        1. ローカル開発環境

        • 自分のPCに開発環境を構築
        • 必要なツール:
          • Node.js/Python
          • AWS CLI
          • コードエディタ(VS Codeなど)
        • メリット:カスタマイズ性が高い

        2. AWS Cloud9

        • AWSが提供するクラウドベースのIDE
        • AWSサービスとの統合が事前設定済み
        • ブラウザから直接開発可能

        3. AWS CloudShell

        • AWSコンソールから直接利用可能なシェル環境
        • AWS認証情報が事前設定済み
        • 基本的なAWSツールが利用可能
        Amazon Bedrock 開発環境の選択肢 ローカル開発環境 – VS Code – AWS CLI – Node.js/Python AWS Cloud9 – クラウドIDE – AWSと統合済み – ブラウザベース AWS CloudShell – ブラウザベースシェル – 事前認証済み – AWSツール搭載済み 必要なコンポーネント AWS SDK – AWS公式ライブラリ – 多言語対応 – APIラッパー 認証情報 – アクセスキーID – シークレットアクセスキー – リージョン設定 Bedrock設定 – モデルアクセス権限 – APIエンドポイント – 使用量設定

        AWS SDKについて

        AWS SDKとは:

        AWS Software Development Kit(SDK)は、AWSの各種サービスをプログラムから利用するためのライブラリ群です。AIに限らず、以下のようなAWSサービス全般を扱えます

        AWS SDK 活用例 Amazon Bedrock テキスト校閲 Claude Titan Text JSON Response 画像・音声生成 Stable Diffusion Titan Image Base64 Output OCR処理 Textract S3連携 Block Detection 集計分析 Claude分析 Comprehend QuickSight連携

        AWS SDKの実行環境

        1. 実行環境の制約
          • AWSコンソール(プレイグラウンド)では実行不可
          • 独自の実行環境が必要
        2. 必要な実行環境の例
          • ローカルPC開発環境
          • AWS Cloud9
          • EC2インスタンス
          • AWS Lambda
        3. 環境構築に必要なもの
          • AWS認証情報(アクセスキー)
          • AWS SDKのインストール
          • 開発言語の実行環境(Node.js, Pythonなど)
        AWS実行環境の違い AWSコンソール (Bedrockプレイグラウンド) できること: ✓ AIモデルとの対話 ✓ 基本的なパラメータ調整 ✓ プロンプトのテスト × 外部サービス連携 × カスタム開発 SDK実行環境 (ローカルPC等) できること: ✓ 全APIアクセス ✓ 外部サービス連携 ✓ カスタムアプリケーション開発 必要なもの: ・AWS認証情報(アクセスキー) 本格的な開発には SDK実行環境の構築が必要

        プログラムからの活用方法

        AWS SDK for JavaScript (Node.js)

        const { BedrockRuntimeClient } = require("@aws-sdk/client-bedrock-runtime");

        AWS SDK for Python (Boto3)

        import boto3
        bedrock = boto3.client('bedrock-runtime')

        必要な認証情報

        開発環境からAWSサービスを利用するには、以下の認証情報が必要です:

        1. アクセスキーID
        2. シークレットアクセスキー
        3. リージョン設定

        プレイグラウンドと開発環境の違い

        プレイグラウンドの制限

        • 基本的なモデル操作のみ
        • 外部サービス連携不可
        • ファイル操作の制限

        開発環境のメリット

        • 外部APIとの連携が可能
        • カスタム機能の実装
        • 自動化やバッチ処理
        • 他のAWSサービスとの統合

        まとめ

        より高度なアプリケーション開発には、適切な開発環境の構築と、AWS SDKを活用した実装が推奨されます。プレイグラウンドは簡単な実験や検証には適していますが、本格的な開発には開発環境の構築をお勧めします。

        Generative AI Use Cases JP (GenU) のセットアップ

        環境準備

        Node.jsのインストール

        AWS CLIのインストール

        AWS CLIとは
        AWSのサービスをコマンドラインから操作するためのツール
        インストール手順はAWS CLI公式インストーラーをダウンロード

        AWSクレデンシャルの設定

        GitHubからClone

        Cloneしたディレクトリでnpm ciを実行

        Bootstrap

        デプロイ

        表示されたUrlにアクセス

        CORSエラー(Cross-Origin Resource Sharing Error)とは?「Access-Control-Allow-Origin」の設定

        CORSエラーとは

        CORSは「異なるオリジン(ドメイン、プロトコル、ポート)間でのリソース共有」を制御するセキュリティの仕組みです。

        そのため異なるオリジン間でリソースをリクエストしようとする際にブラウザが制限をかけることにより、CORSエラーが発生します。

        ウェブサイト localhost:3000 fetch(‘/api/data’) APIサーバー localhost:8000 Access-Control-Allow-* ブラウザ CORSチェック ✓ オリジンの確認 ✓ メソッドの許可 ✓ ヘッダーの確認 CORSエラーが発生する場合: 1. オリジンの不一致(例:localhost:3000からのリクエストに対し、サーバーが異なるオリジンを許可) 2. 許可されていないHTTPメソッド(GET/POST/PUT等)の使用

        「Access-Control-Allow-Origin」の設定

        異なるオリジン間のリクエストで制限をうけずにアクセスするためには、レスポンスヘッダーに「Access-Control-Allow-Origin」を指定することで可能になります。

        https://zenn.dev/syo_yamamoto/articles/445ce152f05b02

        プリフライトリクエストとは

        異なるオリジン(ドメイン)へリクエストを送る前の「事前確認」

        • 本来のリクエストを送る前に、そのリクエストが安全かどうかを確認します
        • HTTPのOPTIONSメソッドを使用して送信されます

        プリフライトリクエストはブラウザが自動的に行います。開発者が明示的にコードを書く必要はありません。

        プリフライトが必要になるケース

        • 特殊なヘッダーを使用する場合
        • application/jsonなどの特殊なContent-Typeを使用する場合
        • GET/POST/HEAD以外のメソッド(PUT、DELETEなど)を使用する場合

        プリフライトリクエストの流れ

        ブラウザがOPTIONSリクエストを送信 サーバーがCORS設定を返答 許可された場合のみ、本来のリクエストを送信

        CORSリクエストパターン パターン1: Simple Request (プリフライトなし) フロントエンド API サーバー GETリクエスト/通常フォーム送信 パターン2: プリフライト成功パターン フロントエンド API サーバー 1. OPTIONSリクエスト 2. CORS OK 3. 本来のリクエスト パターン3: プリフライト失敗パターン フロントエンド API サーバー 1. OPTIONSリクエスト 2. CORS エラー 本来のリクエストは送信されない

        開発者ツールでの確認方法

        1. ブラウザの開発者ツールを開く
        2. Networkタブで確認
          • OPTIONSメソッドのリクエスト → これがプリフライトリクエスト
          • その直後の本来のリクエスト

        リクエストヘッダー:

        • Origin: [送信元のドメイン]
        • Access-Control-Request-Method: [使用予定のメソッド]
        • Access-Control-Request-Headers: [使用予定のヘッダー]

        レスポンスヘッダー:

        • Access-Control-Allow-Origin: [許可されているドメイン]
        • Access-Control-Allow-Methods: [許可されているメソッド]
        • Access-Control-Allow-Headers: [許可されているヘッダー]
        Elements Network Console Name Method Status /api/data OPTIONS 204 /api/data POST 200 プリフライトリクエストの確認方法: 1. ブラウザの開発者ツールを開く (F12 または右クリック → 検証) 2. Networkタブを選択 3. OPTIONSメソッドのリクエストを探す 4. リクエストをクリックすると詳細が表示される: – Request Headers: Origin, Access-Control-Request-Method – Response Headers: Access-Control-Allow-Origin, Methods, Headers

        ローカルのNext.jsのプロジェクトからさくらインターネットのレンタルサーバーのphpのAPIにアクセスする場合

        デジタルイラスト制作ツール徹底比較:Adobe Fresco vs Procreate vs CLIP STUDIO PAINT

        はじめに

        デジタルイラストの世界では、様々な制作ツールが存在します。今回は、主要な3つのソフトウェアの特徴と違いを詳しく見ていきましょう。

        Adobe Fresco

        主な特徴

        • Adobe Creative Cloud との完璧な連携
        • ライブブラシ機能による水彩やオイルの自然な表現
        • ベクターとラスターの両方に対応
        • プロフェッショナル向けの充実した機能

        メリット

        • Photoshopとの互換性が高い
        • クラウド同期で作品管理が容易
        • 実際の画材のような描き心地

        デメリット

        • 月額サブスクリプション制
        • 学習曲線がやや急
        Adobe Fresco 基本情報 価格: ¥1,180/月(CC込み) 対応: iPad / Windows 開発: Adobe Inc. 最新版: 2024年対応 主要機能 ベクター描画 ラスター描画 水彩ブラシ CC連携 最適な用途 • デジタルペインティング • イラスト制作 • コンセプトアート • デザイン作業 特徴的な機能 • リアルな水彩表現 • Photoshop連携 • クラウド同期 • ベクター/ラスター

        Procreate

        主な特徴

        • iPad専用に最適化された直感的なインターフェース
        • カスタムブラシの作成が可能
        • アニメーション機能搭載
        • 手頃な価格の買い切り制

        メリット

        • シンプルで使いやすい
        • 優れたパフォーマンス
        • 豊富なショートカット機能

        デメリット

        • iPad専用のため、他のデバイスでは使用不可
        • レイヤー数に制限あり
        Procreate 基本情報 価格: ¥1,480(買切) 対応: iPad専用 開発: Savage Interactive 最新版: Procreate 6 主要機能 ブラシ機能 レイヤー アニメーション カスタマイズ 最適な用途 • デジタルアート • イラスト制作 • スケッチ • アニメーション 特徴的な機能 • QuickShape • QuickMenu • カスタムブラシ • ジェスチャー操作

        CLIP STUDIO PAINT

        主な特徴

        • マンガ・イラスト制作に特化
        • 充実した3D素材とポーズ集
        • 多彩なブラシと効果
        • クロスプラットフォーム対応

        メリット

        • 専門的な制作に適した豊富な機能
        • アセットストアで素材の入手が容易
        • 安定した動作性能

        デメリット

        • 初心者には機能が多すぎる可能性
        • インターフェースがやや複雑
        CLIP STUDIO PAINT 基本情報 価格: ¥15,000~(買切) 対応: Win/Mac/iPad/Android 開発: セルシス グレード: PRO/EX 主要機能 マンガ機能 3D機能 素材数 アニメーション 最適な用途 • マンガ制作 • イラスト制作 • アニメーション • 商業作品 特徴的な機能 • 3Dポーズ集 • コマ

        選び方のポイント

        1. 用途による選択
        • マンガ制作 → CLIP STUDIO PAINT
        • 一般イラスト → Procreate
        • 商業デザイン → Adobe Fresco
        1. 使用デバイス
        • iPadのみ → Procreate
        • マルチデバイス → CLIP STUDIO PAINTやAdobe Fresco
        1. 予算
        • 買い切り希望 → ProcreateやCLIP STUDIO PAINT
        • サブスクOK → Adobe Fresco
        イラストツール比較 Adobe Fresco Creative Cloud 統合ツール 主な特徴: • ベクター&ラスター対応 • リアルな水彩ブラシ • Photoshop連携 価格: サブスクリプション ¥1,180/月 対応環境: iPad / Windows Procreate iPad専用プロフェッショナルツール 主な特徴: • 直感的なインターフェース • アニメーション機能 • カスタムブラシ作成 価格: 買い切り ¥1,480 対応環境: iPad専用 CLIP STUDIO PAINT マンガ・イラスト制作専門ツール 主な特徴: • マンガ制作機能 • 3D素材対応 • 充実したアセット 価格: 買い切り/サブスク ¥15,000~ 対応環境: Windows / Mac iPad / Android 推奨用途: デザイン・イラスト アート・イラスト マンガ・イラスト

        まとめ

        それぞれのツールに特徴があり、一概にどれが最高とは言えません。自分の用途や好みに合わせて選択することをお勧めします。初心者の方はProcreateから始めるのが良いでしょう。プロフェッショナルな用途ではCLIP STUDIO PAINTやAdobe Frescoが適しています。

        このブログ記事はいかがでしょうか?各ソフトウェアの特徴や比較点について、さらに詳しく知りたい部分がございましたら、お申し付けください。

        Web回覧アプリケーション

        内容

        Chrome開発者ツールで学ぶCookie・セッション管理完全ガイド – 実践的なデバッグ手法と活用例

        はじめに

        Webアプリケーション開発において、Cookie・セッション管理の適切な実装は、セキュリティと快適なユーザー体験の両立に不可欠です。本記事では、Chrome開発者ツールを使用した実践的なデバッグ手法と、一般的な実装上の注意点を詳しく解説します。

        Cookie基礎知識

        Cookieとは

        Cookieは、ブラウザに保存される小さなテキストデータで、以下のような重要な役割を担っています:

        ブラウザのCookieは、ウェブアプリケーションにとって不可欠なデータ保存メカニズムです。ユーザー体験を向上させる多くの機能(ログイン状態の維持、サイト設定の記憶など)の基盤となっています。

        ブラウザ Cookie保存領域 user_id: ABC123 login_status: true theme: dark session_id: xyz789 language: ja Webサーバー セッション管理 ユーザー情報 カート内容 閲覧履歴 認証状態 リクエスト時にCookieを送信 レスポンスでCookieを設定 主な用途 ・ユーザー認証 ・個人設定の保存 ・行動追跡

        Cookieの主な活用シーン

        • ユーザー認証情報の保持
        • カスタマイズ設定の記録
        • アクセス解析とユーザー行動の追跡
        • ショッピングカートの管理

        セキュリティの観点から見たCookieの設定

        Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict

        重要な設定項目:

        • HttpOnly: JavaScriptからのアクセスを防止
        • Secure: HTTPS接続でのみ送信
        • SameSite: クロスサイトリクエストでの送信制御

        Chrome開発者ツールによる高度なデバッグ手法

        Google Chromeの開発者ツールを使ってCookieとセッションの情報を確認する方法を説明します。

        Chrome開発者ツールを起動

        • キーボードショートカット:F12
        • または画面を右クリックして「検証」を選択

        Applicationタブへの移動

        • 上部メニューバーから「Application」を選択
        • 左側メニューから「Storage」→「Cookies」へ進む

        Cookie詳細情報の見方

        開発者ツールで確認できる主要な情報:

        • セキュリティ設定(HttpOnly, Secure)
        • Cookie名称と保存値
        • 有効範囲(Domain)
        • 有効期限情報
        • サイズ制限

        セッション管理の実践

        開発者ツールでのセッション確認

        Cookiesセクションでの確認

        • PHPSESSIDを探す
        • セッションIDの値を確認
        ブラウザ Cookie PHPSESSID: abc123xyz PHPサーバー セッションファイル /tmp/sess_abc123xyz user_id = 12345 login_time = 2024-03-28… cart = [商品A, 商品B, …] last_access = … リクエスト時にPHPSESSIDを送信 PHPSESSIDとは? • PHPがセッションを管理するために使用する一意のID • ブラウザのCookieに保存される • このIDを使ってサーバー上のセッションファイルを特定 • セッションファイルには実際のユーザーデータが保存される

        「Network」タブで確認

        1. 開発者ツールの「Network」タブを開きます。
        2. ページをリロードします。
        3. リクエストの一覧から任意のものをクリックします。「Headers」セクションで、「Request Headers」の中の「Cookie」を確認できます。「Response Headers」の中の「Set-Cookie」でサーバーからのCookie設定を確認できます。

        コンソールを使用したCookie操作

        1. 開発者ツールの「Console」タブを開きます。
        2. JavaScriptコンソールでは以下のような操作が可能:
        // 現在のCookieを表示
        console.log(document.cookie);
        
        // Cookie値の取得
        function getCookie(name) {
            const value = `; ${document.cookie}`;
            const parts = value.split(`; ${name}=`);
            if (parts.length === 2) return parts.pop().split(';').shift();
        }

        セッション管理の実装パターン

        PHPでのセッション管理例:

        session_start();
        $_SESSION['user_id'] = $authenticated_user_id;
        $_SESSION['last_access'] = time();

        セキュリティ対策のベストプラクティス

        1. セッションハイジャック対策
          • 適切なセッションID生成
          • 定期的なセッションローテーション
        2. XSS対策
          • HttpOnly属性の適切な使用
          • 出力エスケープの徹底
        3. CSRF対策
          • トークンの実装
          • SameSite属性の活用

        トラブルシューティング手法

        開発者ツールを使用した一般的な問題の解決方法:

        1. セッション切れの確認
          • Cookieの有効期限チェック
          • サーバーサイドのセッション状態確認
        2. クロスドメイン問題の対処
          • CORS設定の確認
          • クレデンシャルの送信設定
        3. パフォーマンス最適化
          • Cookie容量の最適化
          • 不要なCookieの削除

        まとめ

        Cookie・セッション管理は、現代のWebアプリケーション開発において非常に重要な要素です。Chrome開発者ツールを活用することで、効率的なデバッグと堅牢な実装が可能になります。セキュリティを意識しながら、ユーザー体験の向上につながる適切な実装を心がけましょう。

        この記事では、開発者の実務で直面する具体的な課題に焦点を当て、実践的なソリューションを提供しています。次回は、より高度なセキュリティ対策とパフォーマンス最適化について解説する予定です。

        【chrome】ブラウザ検証画面でWebページのソースコード(ファイル群)をダウンロードする方法

        Chromeブラウザの右上にある3点リーダー →「キャスト、保存、共有」→「名前を付けてページを保存」でダウンロードされます。

        Amazon Bedrock Amazon Titan(AIモデル)を使用して、シンプルなチャットプログラムの作成、応用(テキスト校閲)
        ローカル環境 Node.jsアプリ – 質問入力 – 回答表示 AWS Bedrock Amazon Titan – テキスト生成 – 自然言語処理 質問を送信 回答を受信

        シンプルなチャットプログラムの作成

        Node.jsのプロジェクトを初期化

        npm init -y

        @aws-sdk/client-bedrock-runtime

        Bedrock Runtime API のリクエスト作成と送信するために@aws-sdk/client-bedrock-runtimeをインストール

        npm install @aws-sdk/client-bedrock-runtime
        • Node.js環境でAWS Bedrockサービスと通信するための公式ライブラリ(AWSが提供する公式SDK)
        • 【開発環境の構築が容易】Node.jsのインストールだけで開発開始可能
        • フロントエンド開発との親和性が高い、共通の言語(JavaScript/TypeScript)使用

        1)Node.jsライブラリ(データの前処理や基本的な処理)

        • Tesseract.js(OCR)
        • Express(APIサーバー)
        • Multer(ファイルアップロード)
        • Sharp(画像処理)

        2)プロンプト:AIに適切な指示を出す

        App.jsを作成

        // AWS SDKから必要な機能をインポート
        // BedrockRuntimeClient: AWSのBedrockサービスに接続するためのクライアント
        // InvokeModelCommand: AIモデルを呼び出すためのコマンド
        const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime');
        
        // AWS Bedrockクライアントの設定
        // region: AWSのサービスを使用するリージョン(地域)
        // credentials: AWS認証情報
        const client = new BedrockRuntimeClient({
            region: "us-east-1",  // 米国東部(バージニア)リージョン
            credentials: {
                accessKeyId: "あなたのアクセスキー",      // AWSアクセスキー
                secretAccessKey: "あなたのシークレットキー" // AWSシークレットキー
            }
        });
        
        // AIに質問するための関数
        // async/await: 非同期処理を扱うための構文
        async function askAI(question) {
            try {
                // AIモデルへのリクエスト内容を設定
                const input = {
                    // 使用するAIモデルを指定(Amazon Titan)
                    modelId: "amazon.titan-text-express-v1",
                    
                    // データの形式をJSON形式に指定
                    contentType: "application/json",
                    accept: "application/json",
                    
                    // AIモデルへの具体的な指示内容
                    body: JSON.stringify({
                        // 質問文を設定
                        inputText: question,
                        
                        // AIの応答設定
                        textGenerationConfig: {
                            maxTokenCount: 1000,    // 最大応答長
                            temperature: 0.7        // 応答の創造性(0-1: 高いほど創造的)
                        }
                    })
                };
        
                // AIモデルを呼び出すコマンドを作成
                const command = new InvokeModelCommand(input);
                
                // コマンドを実行してAIからの応答を取得
                const response = await client.send(command);
                
                // AIからの応答(バイナリ)をテキストに変換
                const responseBody = JSON.parse(new TextDecoder().decode(response.body));
                
                // 応答を表示
                console.log("回答:", responseBody.results[0].outputText);
                
            } catch (error) {
                // エラーが発生した場合の処理
                console.error("エラーが発生しました:", error);
            }
        }
        
        // プログラムのテスト実行
        // ここの質問を変更することで、違う質問ができます
        askAI("こんにちは!簡単な自己紹介をしてください。");
        時間の流れ async function askAI() await client.send(command) AIからの応答待ち 応答を表示 1. 非同期関数の開始 2. AIモデルの呼び出しを待機 3. 応答を受け取って処理

        AWSが提供しているclient.send()というメソッドが 内部でPromiseを使用していて 私たちはそれをawaitで待っている

        AWSのアクセスキーとシークレットキーを取得する

        IAMユーザーの作成

        1. 「ポリシーを直接アタッチする」を選択
        2. 検索ボックスに「Bedrock」と入力
        3. 「AmazonBedrockFullAccess」にチェック
        4. 「次へ」をクリック
        1. 作成したユーザー名をクリック
        2. 「セキュリティ認証情報」タブを選択
        3. 「アクセスキーを作成」ボタンをクリック
        4. 「ユースケース」で「サードパーティーのサービス」を選択
        5. 警告の確認チェックボックスにチェック
        6. 「次へ」をクリック

        ファイルを実行

        先ほど作成したapp.jsに取得したキーに更新してファイルを下記の通り実行すると、、

        node app.js
        回答: 
        私はAIです。人工知能の一種で、あなたの質問に答えたり、あなたの指示に従ったりすることができます。
        私はあなたとチャットできることをうれしく思います。

        テキスト校閲

        使っているもの:

        1. AWS Bedrockのサービス
        2. Amazon TitanのAIモデル
        3. AWS SDKのクライアント
        async function checkText(text) {
            try {
                // 1. AIへの指示(プロンプト)を作成
                const input = {
                    modelId: "amazon.titan-text-express-v1",  // Amazon TitanのAIモデル
                    contentType: "application/json",
                    accept: "application/json",
                    body: JSON.stringify({
                        // ここが重要:AIへの指示内容
                        inputText: `
                        以下の文章を校閲してください。
                        - 誤字脱字
                        - 文法の間違い
                        - より良い表現の提案
                        を箇条書きで指摘してください。
        
                        文章:${text}`,
                        textGenerationConfig: {
                            maxTokenCount: 1000,
                            temperature: 0.7
                        }
                    })
                };
        
                // 2. AIに送信して結果を待つ
                const command = new InvokeModelCommand(input);
                const response = await client.send(command);
                
                // 3. 結果を表示
                const responseBody = JSON.parse(new TextDecoder().decode(response.body));
                console.log("校閲結果:", responseBody.results[0].outputText);
            } catch (error) {
                console.error("エラーが発生しました:", error);
            }
        }

        上記のの校閲処理は特別な校閲ロジックは実装していないので、全てAIモデル(プロンプトの指示)に依存しています

        より高度なプログラムにするために下記のようにします

        • 独自の校閲ルール
        • 形態素解析
        • 文法チェックライブラリ などを組み合わせる
        ComprehensiveTextChecker メインコントローラー CustomChecker MorphAnalyzer GrammarChecker
        入力テキスト チェック処理 独自ルールチェック 形態素解析 文法チェック AIチェック チェック結果 – 表記の修正 – 文法エラー – 形態素情報 – AI提案

        // 1. 独自の校閲ルールを追加
        const customRules = {
            "です/ます": "敬語の統一をチェック",
            "こと/もの": "表現の統一をチェック",
            // ... その他のルール
        };
        
        // 2. 形態素解析ライブラリの使用
        // npm install kuromoji
        const kuromoji = require('kuromoji');
        
        // 3. 独自の校閲ロジックを実装
        async function advancedCheck(text) {
            // AIによる校閲
            const aiResult = await checkText(text);
            
            // 独自ルールによるチェック
            const customResult = applyCustomRules(text);
            
            // 形態素解析による確認
            const morphResult = await morphologicalAnalysis(text);
            
            return {
                ai: aiResult,
                custom: customResult,
                morph: morphResult
            };
        }

        独自の校閲ルール

        // 独自の校閲ルールの実装例
        class TextChecker {
            constructor() {
                // 基本的なルール定義
                this.rules = {
                    // 表記ゆれチェック
                    styleRules: {
                        'とっても': 'とても',
                        'みたい': 'ような',
                        'すごく': '大変'
                    },
                    
                    // 文末表現チェック
                    endingRules: {
                        'です・ます': /([てで]います|てます|です)(?![かが])/,
                        'である': /である(?![かが])/
                    },
        
                    // ビジネス文書チェック
                    businessRules: {
                        '不適切': ['思います', 'そうです', 'だと思います'],
                        '推奨': ['考えられます', '示唆されます', '推察されます']
                    }
                };
            }
        
            // テキストをチェックするメソッド
            checkText(text) {
                const results = [];
                
                // 表記ゆれチェック
                for (const [incorrect, correct] of Object.entries(this.rules.styleRules)) {
                    if (text.includes(incorrect)) {
                        results.push(`「${incorrect}」は「${correct}」に修正することを推奨します`);
                    }
                }
        
                // その他のルールも同様にチェック
                return results;
            }
        }

        形態素解析

        // npm install kuromoji
        const kuromoji = require('kuromoji');
        
        class MorphologicalAnalyzer {
            async initialize() {
                return new Promise((resolve, reject) => {
                    kuromoji.builder({ dicPath: 'node_modules/kuromoji/dict' })
                        .build((err, tokenizer) => {
                            if (err) {
                                reject(err);
                            } else {
                                this.tokenizer = tokenizer;
                                resolve();
                            }
                        });
                });
            }
        
            analyze(text) {
                const tokens = this.tokenizer.tokenize(text);
                return this.processTokens(tokens);
            }
        
            processTokens(tokens) {
                const analysis = {
                    wordTypes: {},      // 品詞の分布
                    conjugations: [],   // 活用の問題
                    suggestions: []     // 改善提案
                };
        
                tokens.forEach(token => {
                    // 品詞の集計
                    const pos = token.pos;
                    analysis.wordTypes[pos] = (analysis.wordTypes[pos] || 0) + 1;
        
                    // 活用のチェック
                    if (token.pos === '動詞' && token.conjugated_form !== '基本形') {
                        analysis.conjugations.push({
                            word: token.surface_form,
                            basic: token.basic_form,
                            type: token.conjugated_form
                        });
                    }
                });
        
                return analysis;
            }
        }

        文法チェックライブラリ

        // npm install textlint textlint-rule-preset-japanese
        const { TextLintEngine } = require('textlint');
        
        class GrammarChecker {
            constructor() {
                this.engine = new TextLintEngine({
                    rules: {
                        // プリセットルールの使用
                        'preset-japanese': true,
                        // カスタムルールの追加
                        'max-ten': {
                            // 一文での「、」の数を制限
                            max: 3
                        },
                        'sentence-length': {
                            // 一文の長さを制限
                            max: 100
                        }
                    }
                });
            }
        
            async check(text) {
                try {
                    const results = await this.engine.executeOnText(text);
                    return this.formatResults(results);
                } catch (error) {
                    console.error('文法チェックエラー:', error);
                    return [];
                }
            }
        
            formatResults(results) {
                return results[0].messages.map(message => ({
                    line: message.line,
                    column: message.column,
                    message: message.message,
                    ruleId: message.ruleId
                }));
            }
        }

        組み合わせた場合

        class ComprehensiveTextChecker {
            constructor() {
                this.customChecker = new TextChecker();
                this.morphAnalyzer = new MorphologicalAnalyzer();
                this.grammarChecker = new GrammarChecker();
            }
        
            async initialize() {
                await this.morphAnalyzer.initialize();
            }
        
            async checkText(text) {
                const results = {
                    customRules: this.customChecker.checkText(text),
                    morphology: await this.morphAnalyzer.analyze(text),
                    grammar: await this.grammarChecker.check(text)
                };
        
                // AIの結果と組み合わせる
                const aiResults = await checkText(text);  // 先ほどのAI機能
                results.ai = aiResults;
        
                return this.summarizeResults(results);
            }
        
            summarizeResults(results) {
                // 結果を整理して返す
                return {
                    suggestions: [
                        ...results.customRules,
                        ...results.grammar.map(g => g.message)
                    ],
                    analysis: {
                        wordTypes: results.morphology.wordTypes,
                        grammarIssues: results.grammar.length,
                        aiSuggestions: results.ai
                    }
                };
            }
        }

        使用例

        async function main() {
            const checker = new ComprehensiveTextChecker();
            await checker.initialize();
        
            const text = "私はとっても疲れたので、今日は早く帰りたいと思います。";
            const results = await checker.checkText(text);
            console.log(results);
        }

        補足)フォルダ構成

        text-checker-project/
        │
        ├── src/                      # ソースコードのメインディレクトリ
        │   ├── checkers/            # 各チェッカーの実装
        │   │   ├── CustomChecker.js    # 独自ルールチェッカー
        │   │   ├── MorphAnalyzer.js    # 形態素解析
        │   │   ├── GrammarChecker.js   # 文法チェッカー
        │   │   └── AIChecker.js        # AI利用のチェッカー
        │   │
        │   ├── rules/               # ルール定義
        │   │   ├── styleRules.js       # 表記ゆれルール
        │   │   ├── grammarRules.js     # 文法ルール
        │   │   └── businessRules.js    # ビジネス文書ルール
        │   │
        │   ├── utils/               # ユーティリティ関数
        │   │   ├── formatter.js        # 結果フォーマット
        │   │   └── logger.js           # ログ処理
        │   │
        │   └── index.js             # メインのエントリーポイント
        │
        ├── config/                  # 設定ファイル
        │   ├── default.json           # デフォルト設定
        │   └── textlint-rules.json    # textlintルール設定
        │
        ├── tests/                   # テストファイル
        │   ├── customChecker.test.js
        │   ├── morphAnalyzer.test.js
        │   └── grammarChecker.test.js
        │
        ├── examples/                # 使用例
        │   └── basic-usage.js
        │
        ├── docs/                    # ドキュメント
        │   ├── API.md
        │   └── RULES.md
        │
        ├── node_modules/            # 依存パッケージ
        ├── package.json
        ├── package-lock.json
        └── README.md

        src/index.js

        const ComprehensiveTextChecker = require('./checkers/ComprehensiveChecker');
        const config = require('../config/default.json');
        
        module.exports = ComprehensiveTextChecker;

        src/checkers/CustomChecker.js

        const styleRules = require('../rules/styleRules');
        const businessRules = require('../rules/businessRules');
        
        class CustomChecker {
            constructor(config = {}) {
                this.rules = {
                    style: styleRules,
                    business: businessRules,
                    ...config.rules
                };
            }
        
            checkText(text) {
                // 実装
            }
        }
        
        module.exports = CustomChecker;

        src/rules/styleRules.js

        module.exports = {
            // 表記ゆれルール
            corrections: {
                'とっても': 'とても',
                'みたい': 'ような'
            },
            // その他のルール
        };

        package.json

        {
          "name": "text-checker",
          "version": "1.0.0",
          "main": "src/index.js",
          "scripts": {
            "start": "node src/index.js",
            "test": "jest",
            "lint": "eslint src/"
          },
          "dependencies": {
            "kuromoji": "^1.0.0",
            "textlint": "^12.0.0",
            "textlint-rule-preset-japanese": "^7.0.0",
            "@aws-sdk/client-bedrock-runtime": "^3.0.0"
          },
          "devDependencies": {
            "jest": "^27.0.0",
            "eslint": "^8.0.0"
          }
        }

        examples/basic-usage.js

        const TextChecker = require('../src/index');
        
        async function example() {
            const checker = new TextChecker();
            await checker.initialize();
        
            const text = "私はとっても疲れたので、今日は早く帰りたいと思います。";
            const results = await checker.checkText(text);
            console.log(results);
        }
        
        example().catch(console.error);
        【Next.js】ハイドレーションエラーSuspenseで解決

        非同期データの取得はuseEffectかSuspenseで管理

        ハイドレーションエラー

        ハイドレーションの流れとエラー 1. SSR HTMLを生成 {“name”: “John”} {“time”: “12:00”} 2. 初期HTML表示 ブラウザ表示 {“name”: “John”} {“time”: “12:00”} 3. ハイドレーション Reactの初期化 {“name”: “John”} {“time”: “12:01”} 一般的なエラーと解決策 ⚠️ window/documentの使用 ✅ useEffectで初期化 ⚠️ 日時の不一致 ✅ サーバー時刻の固定 ⚠️ 非同期データ ✅ Suspense使用 主なポイント 1. サーバー/クライアントの一致確認 2. 適切なローディング状態の管理

        Suspenseが効果的な場合

        • 非同期データの取得待ち
        • 動的インポート
        • 画像やその他のリソースのロード待ち
        Suspense による制御 Loading… データ取得中 表示完了
        【Laravel 】PHPUnit/Laravel

        テストとはアプリケーションが正しく動作することを確認する自動チェックです

        テスト

        Laravel

        • 自動なんで、人間がやらなくていい(コマンドで走らせる)
        • レッドグリーンテスト
        • テスト駆動開発(Test-Driven Development: TDD)テストファーストなプログラムの開発手法
        • テストコードを書くのに開発と同じ時間がかかるので、工数的には膨らむ。
        【Laravel】Sanctumでシンプルな認証

        トークンベースの認証により、ステートレスなAPI通信が可能

        シンプルなメモアプリのAPI

        1. ログイン email + password → トークン取得 2. メモ作成 認証済みユーザーのみ メモの作成が可能 3. メモ取得 自分のメモのみ 取得可能 POST /api/login → トークン取得 POST /api/memos → メモ作成 (要トークン) GET /api/memos → メモ一覧取得 (要トークン)
        // routes/api.php
        // ユーザー登録
        Route::post('/register', function (Request $request) {
            $user = User::create([
                'name' => $request->name,
                'email' => $request->email,
                'password' => Hash::make($request->password),
            ]);
            
            $token = $user->createToken('auth-token')->plainTextToken;
            
            return response()->json([
                'token' => $token
            ]);
        });
        
        // ログイン
        Route::post('/login', function (Request $request) {
            $user = User::where('email', $request->email)->first();
            
            if (!$user || !Hash::check($request->password, $user->password)) {
                return response()->json(['message' => 'Unauthorized'], 401);
            }
            
            $token = $user->createToken('auth-token')->plainTextToken;
            
            return response()->json([
                'token' => $token
            ]);
        });
        // routes/api.php
        Route::middleware('auth:sanctum')->group(function () {
            // メモの作成
            Route::post('/memos', function (Request $request) {
                $memo = $request->user()->memos()->create([
                    'title' => $request->title,
                    'content' => $request->content
                ]);
                
                return response()->json($memo);
            });
            
            // メモの取得
            Route::get('/memos', function (Request $request) {
                $memos = $request->user()->memos;
                return response()->json($memos);
            });
        });
        // ログイン
        async function login() {
            const response = await fetch('api/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    email: 'user@example.com',
                    password: 'password123'
                })
            });
            const data = await response.json();
            // トークンを保存
            localStorage.setItem('token', data.token);
        }
        
        // メモを作成
        async function createMemo() {
            const response = await fetch('api/memos', {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${localStorage.getItem('token')}`,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    title: '新しいメモ',
                    content: 'メモの内容'
                })
            });
            const memo = await response.json();
            console.log('作成されたメモ:', memo);
        }
        Amazon Bedrockとは

        Amazon Bedrockとは

        Amazon Bedrockは、さまざまなAIモデルを簡単に利用できるようにするAWSのサービスです

        料金

        基本的な課金構造

        • 入力トークン数と出力トークン数で別々に課金
        • 1,000トークンあたりの料金で計算
        • 実際の使用量のみ請求(最低利用額なし)
        モデル バリアント 入力料金
        (1Kトークン)
        出力料金
        (1Kトークン)
        備考
        Claude (Anthropic) Claude Instant 0.80円 2.40円 最も手頃な選択肢
        Claude (Anthropic) Claude 3 Sonnet 3.00円 15.00円 バランスの取れたモデル
        Claude (Anthropic) Claude 3 Opus 15.00円 75.00円 最高性能モデル
        Llama 2 (Meta) Llama 2 13B 0.60円 0.80円 オープンソースベース
        Titan (Amazon) Titan Text Lite 0.30円 0.40円 軽量な処理向け
        1. 言語による違い
        • 英語:1単語≒1-2トークン
        • 日本語:1文字≒2-3トークン
        • 記号や空白:0.2-0.5トークン程度
        1. 一般的な目安
        • チャット1往復:100-300トークン
        • メール1通:200-400トークン
        • 技術文書1ページ:500-800トークン

        コスト最適化のポイント

        • 入力は簡潔にすることでコストを抑えられる
        • システムプロンプトの再利用で入力コストを削減
        • 出力制限を設定して想定外の高額請求を防止

        ハンズオン

        AWSマネジメントコンソール経由

        マネジメントコンソールにログイン

        リージョンをバージニア北部にします
        → 使用できるプレイグラウンドに影響

        「Amazon Bedrock」を選択

        Amazon Bedrock をマネジメントコンソールから利用するには、最初に Base Models(基盤モデル)へのアクセスを設定する必要があります。

        「モデルアクセス」

        使用するモデルのアクセスを設定
        → 「✅アクセスが付与されました」となります。

        「プレイグラウンド」から「Chat/text」を選択

        「モデルを選択」

        チャットで回答してくれました

        「イメージ」で画像生成も可能

        「AWS SDK」を実行しAmazon Bedrock APIを呼び出す

        CloudShellを起動

        すぐに使える主要なAIモデル

        • Anthropicの「Claude」(テキスト生成)
        • Meta「Llama 2」(言語モデル)
        • Stability AI「Stable Diffusion」(画像生成)
        • AmazonのTitanモデル など

        DifyとAmazon Bedrockの違い

        サービスの性質

        • Dify:
          • オープンソースのAIアプリケーション開発プラットフォーム
          • 自社でホストすることも可能
          • プロンプトエンジニアリングに特化
          • より小規模なプロジェクトに適している
        • Amazon Bedrock:
          • AWSの完全マネージドサービス
          • エンタープライズ向けの機能が充実
          • 大規模なスケーリングが可能
          • AWSの他サービスとの連携が容易

        主な用途

        • Dify:
          • チャットボットの作成
          • プロンプトの管理と最適化
          • シンプルなAIアプリケーションの開発
          • プロトタイピング
        • Amazon Bedrock:
          • 大規模なAIサービスの展開
          • 複数のAIモデルの統合
          • エンタープライズアプリケーションの開発
          • 高セキュリティな環境でのAI活用

        料金体系

        • Dify:
          • オープンソースで基本無料
          • 自身でホストする場合はインフラ費用のみ
          • クラウドホスティング版は従量課金
        • Amazon Bedrock:
          • AWS従量課金制
          • 利用するAIモデルごとに料金が異なる
          • エンタープライズ向けの価格帯
        【WordPress PHPエラー】Fatal error: Allowed memory size of xxx bytes exhausted

        Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 430080 bytes) in /home/xxxx/public_html/xxxxx.com/wp-includes/wp-db.php on line xxxx

        WordPressで固定ページを編集しようと編集をクリックすると上記エラーが発生してしまいました。

        PHPのメモリ不足が原因のようです

        詳しく言うと、許可されているメモリサイズ(268435456 bytes = 256MB)を使い果たして、

        さらに397312 bytes(約388KB)の追加メモリ割りあてようとしたら失敗したようです

        おそらく原因は固定ページ内に大量の画像ブロックを配置しているから

        WordPressの編集画面でのメモリ問題 500KB × 400枚… メモリ使用量 PHPのメモリ制限に近い状態 発生する問題: 1. 編集画面を開く時にメモリエラー 2. 保存時にメモリエラー 3. 画面の応答が遅い
        https://internet.mints.ne.jp/%e3%83%af%e3%83%bc%e3%83%89%e3%83%97%e3%83%ac%e3%82%b9%e3%81%ae%e3%83%a1%e3%83%87%e3%82%a3%e3%82%a2%e3%81%ab%e4%bf%9d%e5%ad%98%e3%81%97%e3%81%9f%e7%94%bb%e5%83%8f%e3%81%a8%e3%83%87%e3%83%bc%e3%82%bf

        メモリの上限

        メモリの制限は複数の層で設定されています

        レンタルサーバー(PHP)の制限 php.ini による制限 (例:256MB) WordPress の制限 wp-config.php による制限 WordPressの制限は、PHPの制限を超えることができません
        1. レンタルサーバー(PHP)の制限
        • サーバーのphp.iniファイルで設定される制限
        • サーバー全体のPHPスクリプトに適用される最大メモリ使用量
        • レンタルサーバーの管理画面やFTPでphp.iniをアップロードすることで変更可能
        1. WordPressの制限
        • WordPressのwp-config.phpファイルで設定される制限
        • PHPの制限を超えて設定することはできない
          • 例:PHPの制限が256MBの場合、WordPressで512MBと設定しても256MBまでしか使用できない

        1)サーバー全体のPHPで設定されるメモリ制限

        これが最も上位の制限となり、この値を超えることはできない

        通常、レンタルサーバーの管理画面やサポートでのみ変更可能

        メモリの確認方法

        ルードディレクトリに下記の様なphpファイルを設置しアクセスし、memory_limitを確認できます

        info.php

        <?php phpinfo(); ?>

        ↓アクセスしてみるとメモリ上限が確認できます

        php.iniで上限をふやせます

        お名前ドットコムをレンタルサーバーで使用している場合は下記の記事を参考できます。

        ご利用サーバーのFTPサーバーへご接続いただき、該当ドメインディレクトリ直下に
        エラーの内容以上のメモリサイズを記述した「php.ini」ファイルを設置(アップロード)してください。
        ※既に「php.ini」ファイルを設置されている場合には、同ファイルを修正してください。

        https://help.onamae.com/answer/20364

        レンタルサーバー側からメモリの使用が集中してアクセス制限がかかることがあります

        「ERR_CONNECTION_TIMED_OUT」エラーの解決方法

        https://help.onamae.com/answer/20364

        2)WordPress側のメモリ

        • WP_MEMORY_LIMIT:通常時の制限(デフォルト40M)
        • WP_MAX_MEMORY_LIMIT:管理画面での制限(デフォルト256M)

        ワードプレスのメモリは管理画面のツールのサイトヘルスから確認できます

        一時的にメモリ制限を大幅に引き上げる方法:

        // wp-config.phpに追加
        define('WP_MEMORY_LIMIT', '256M');
        define('WP_MAX_MEMORY_LIMIT', '512M');

        wp-config.php冒頭の<?phpの次の行に追記しました

        ↓サイトヘルスで変更が確認できました

        https://sologaku.com/wordpress/how-to-change-wordpress-memory-limit/#google_vignette

        対処法

        今回のエラー

        Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate…

        はどちらのエラー??

        Fatal error: [エラーの説明] in [ファイルのパス] on line [行番号]

        はPHPのエラーメッセージの形式

        WordPressメディアライブラリから画像データそのものを削除すると、固定ページ上のギャラリーブロックに関連付けられていた画像の参照が切れ、メモリ消費が減少するため、編集画面に遷移できる可能性が高い

        ただし、以下の点に注意が必要です:

        1. メディアライブラリから画像を削除した場合:
        • 固定ページ上のギャラリーブロックは残りますが、削除した画像は「見つかりません」や空の枠として表示される
        • ブロックエディタで開くと、画像が欠落したギャラリーブロックとして表示される
        • サイト上では画像が表示されなくなる
        1. 推奨される手順:
        • まずサイトとデータベースのバックアップを取る
        • メディアライブラリから、問題のページで使用している画像を一部削除
        • 編集画面にアクセスできるようになったら、不要なギャラリーブロックを整理
        • 必要な画像は最適化して再アップロード
        1. より安全な代替アプローチ:
        • まず、wp-config.phpでメモリ制限を一時的に引き上げてから
        • 画像の整理と最適化を行う方が、データの制御がしやすい

        このアプローチで編集画面にアクセスできるようになったら、今後のために:

        必要に応じてギャラリーの分割 を検討することをお勧めします。

        画像サイズの最適化

        1ページあたりの画像数の制限

        ワードプレスのデバッグモードを有効にして詳細なエラー情報を確認:

        // wp-config.phpに追加
        define('WP_DEBUG', true);
        define('WP_DEBUG_LOG', true);

        遅延読み込み(Lazy Loading)

        遅延読み込みとは: 画面に表示されている部分の画像だけを読み込み、見えていない部分の画像は後回しにする仕組みです。

        表示エリア(画像読み込み済み) スクロール下の画像(未読み込み)
        // functions.php
        function add_lazy_loading_to_admin() {
            // 管理画面でのみ実行
            if (is_admin()) {
                // インラインでJavaScriptを追加
                wp_add_inline_script('jquery', '
                    jQuery(document).ready(function($) {
                        // 画像を監視するための設定
                        const observer = new IntersectionObserver((entries) => {
                            entries.forEach(entry => {
                                if (entry.isIntersecting) {
                                    const img = entry.target;
                                    if (img.dataset.src) {
                                        img.src = img.dataset.src;
                                        img.removeAttribute("data-src");
                                    }
                                }
                            });
                        });
        
                        // 編集画面内の画像を処理
                        $("#post-body img").each(function() {
                            const img = $(this);
                            const originalSrc = img.attr("src");
                            img.attr("data-src", originalSrc);
                            img.attr("src", "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7");
                            observer.observe(img[0]);
                        });
                    });
                ');
            }
        }
        add_action('admin_enqueue_scripts', 'add_lazy_loading_to_admin');

        リスクについて

        リスクは比較的低いです。理由は:

        1. 編集画面のみの変更
          • 公開サイトには影響なし
          • 画像データ自体は変更しない
        2. 簡単に元に戻せる
          • functions.phpから該当コードを削除するだけ
        3. エラーが起きても
          • 最悪の場合、画像が表示されないだけ
          • WordPressの基本機能は影響を受けない

        筆者はこの方法ではメモリ数は抑えることはできませんでした…

        データベースクエリとリビジョンを対策

        データベースクエリとは:

        • WordPressがデータベースにデータを要求する際の命令
        • 例:
          • 記事の内容を取得
          • 画像情報の取得
          • カスタムフィールドの取得
        • 一つのページを表示するために、多数のクエリが実行される
        • 各クエリの結果はメモリに保存される

        リビジョンとは:

        • 記事の変更履歴を保存する機能
        • 保存する度に新しいバージョンが作られる
        • デフォルトでは無制限に保存される
        • 各バージョンがデータベースに保存され、編集画面で読み込まれる
        WordPressのメモリ消費要因 データベースクエリ 投稿データの取得 メタデータの取得 カスタムフィールドの取得 リビジョン 最新版 1つ前のバージョン 2つ前のバージョン メモリ消費要因の説明: 1. データベースクエリ: ・1回のページ表示で数十〜数百のクエリが実行 ・各クエリ結果がメモリに保存される 2. リビジョン: ・記事の変更履歴が全て保存される ・各バージョンの内容がメモリに読み込まれる

        データベースクエリの設定を最適化

        WordPressのデフォルト設定と最適化後 デフォルト設定 取得データ一覧 ・ID ・post_title(タイトル) ・post_content(本文) ・post_excerpt(抜粋) ・post_status(状態) ・comment_status(コメント設定) ・ping_status(ピング設定) ・post_password(パスワード) ・post_name(スラッグ) ・to_ping(ピング先) ・pinged(ピング済み) 最適化後 必要最小限のデータ ・ID ・post_title(タイトル) ・post_content(本文)
        // 必要な項目のみを取得するように変更
        function optimize_post_queries() {
            add_filter('posts_fields', function($fields) {
                global $wpdb;
                // 必要最小限のフィールドのみを指定
                return "{$wpdb->posts}.ID, {$wpdb->posts}.post_title, {$wpdb->posts}.post_content";
            });
        }
        add_action('init', 'optimize_post_queries');

        リスク:高い

        WordPressのコア処理に介入するため、エラーが発生すると編集画面が開けなくなる可能性がある

        リビジョンの最適化

        リビジョンのデフォルト設定:

        • デフォルトでは無制限(制限なし)
        • 自動保存は60秒間隔
        • 全てのリビジョンがデータベースに保存される
        // wp-config.php に追加するだけ
        define('WP_POST_REVISIONS', 5);  // リビジョンを5個に制限

        上記の手順ではリビジョンの設定変更後、既存のリビジョンは自動的には削除されません

        プラグイン「WP-Sweep」

        筆者の編集画面でのエラーは解消されましたが、画像を削除している最中何回か保存したら下記アクセス制限がかかりました。

        LaravelのバージョンLaravel 10とLaravel 11の主な違い

        Laravelの開発環境をSailerを使って構築する方法

        Docker Desktopを起動しておく

        ターミナルでUbuntuをコマンドライン環境として選択する(理由:本番環境でLinuxを使う、Laravelの定番)

        ▼ 選択 PowerShell Command Prompt Ubuntu Azure Cloud Shell ここからUbuntuを 選択してください

        プロジェクト作成コマンド

        curl -s "https://laravel.build/laravel-study?with=mysql,mailpit" | bash

        Dockerを起動しているのに「Docker is not running.」となってしまったら、下記の通りDocker desktopの設定を変更してみてください

        ./vendor/bin/sail up -d
        ① curl:
        - ウェブ上からデータをダウンロードするためのツール
        - この場合、Laravel Sailのインストールスクリプトを取得している
        
        ② -s:
        - silentモード(進行状況などを表示しない)
        - ダウンロード時の詳細な情報を省略する設定
        
        ③ https://laravel.build/:
        - Laravel Sailの自動セットアップスクリプトが置かれているURL
        
        ④ laravel-study:
        - 作成するプロジェクトの名前
        - この部分は自由に変更可能(例:my-app, blog-systemなど)
        
        ⑤ ?with=mysql,mailpit:
        - 一緒にインストールする追加機能の指定
        - mysql:データベース
        - mailpit:メール送信のテスト環境
        
        ⑥ | bash:
        - ダウンロードしたスクリプトをすぐに実行する指示

        プロジェクト初期設定

        config\app.phpファイル

            'timezone' => env('APP_TIMEZONE', 'UTC'),
        
        
            'locale' => env('APP_LOCALE', 'en'),

        env関数

        .env( 設定項目名, デフォルト値)

        .envファイル

        # APP_TIMEZONE=UTC
        APP_TIMEZONE=Asia/Tokyo
        APP_URL=http://localhost
        
        # APP_LOCALE=en
        APP_LOCALE=ja

        コントローラーの使い方と目的

        ./vendor/bin/sail artisan make:controller UtilityController
        https://biz.addisteria.com/laravel11_upgrade/
        さくらインターネットのデータベースにさくらのWebサーバ以外からAPI経由でアクセスする方法

        さくらインターネットのWebサーバー外部から直接アクセスできない

        直接MySQLへの接続はブロックされています(セキュリティ対策)

        解決策:API経由の接続

        ローカルPC Next.jsアプリ さくらサーバー PHP API MySQL データベース ❌ 直接接続は制限されています 1. APIリクエスト 2. DB接続 3. データ取得 4. JSON応答

        ローカルNext.jsからアクセスする場合

        さくらインターネットの公開ディレクトにusers-api.phpを配置

        <?php
        header('Content-Type: application/json');
        
        $host = 'mysql80.xxxx.sakura.ne.jp';
        $dbname = 'xxxx';
        $user = 'xxxx';
        $pass = 'xxxx';
        
        try {
            $pdo = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass);
            $stmt = $pdo->query('SELECT id, email, name FROM users');
            $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
            
            echo json_encode([
                'success' => true,
                'data' => $users
            ]);
        
        } catch(PDOException $e) {
            echo json_encode([
                'success' => false,
                'error' => $e->getMessage()
            ]);
        }
        ?>

        Next.jsから上記APIを呼び出す

        app/api/users/route.ts

        import { NextResponse } from "next/server";
        
        export async function GET() {
          try {
            const response = await fetch('https://あなたのドメイン/users-api.php');
            const data = await response.json();
            return NextResponse.json(data);
          } catch (error) {
            return NextResponse.json({ 
              error: 'ユーザー情報の取得に失敗しました' 
            }, { status: 500 });
          }
        }

        参考サイト

        https://qiita.com/a1c/items/33757c0f5b3c3233450f
        LaravelでMySQLを使用したシンプルな認証システム (さくらインターネットDB作成手順)

        MVCのルーティングの流れ

        処理の流れ:

        1. ブラウザからリクエスト:
          • ユーザーがURLにアクセス
        2. Routeが処理を振り分け:
          • URLに合わせて適切なControllerを呼び出し
        3. ModelでDBアクセス:
          • 必要なデータを取得/保存
        4. Viewで画面を作成:
          • HTMLを生成
        5. ブラウザに結果を返す:
          • 作成した画面を表示
        ブラウザ example.com/users Route /users → Controller Controller 処理の制御 Model データベース操作 View 画面表示 1 2 3 4 5

        まずはデータベースを作成

        さくらインターネットでMySQLの用意をします

        (さくらインターネットのレンタルサーバーでは、標準でMySQLが提供されています)

        さくらのコントロールパネルにログイン

        「データベース」メニューを選択

        「新規作成」をクリック

        データベース名、ユーザー名(データベース同一)、パスワードを設定

        新規作成したDBを選択し「設定」から「phpMyAdmin」をクリック

        Laravelプロジェクトでの設定

        .envファイルでデータベース接続の設定

        .envファイルを以下のように設定します

        DB_CONNECTION=mysql
        DB_HOST=mysql○○.db.sakura.ne.jp    # さくらインターネットから提供されるホスト名
        DB_PORT=3306
        DB_DATABASE=データベース名           # さくらで作成したDB名
        DB_USERNAME=ユーザー名              # さくらで設定したユーザー名
        DB_PASSWORD=パスワード              # さくらで設定したパスワード
        ① さくらサーバー ・DB作成 ・ユーザー作成 ② .env設定 ・DB_HOST ・DB_DATABASE ③ 接続確認 php artisan tinker DB接続テスト

        Tinker でのDB接続確認方法

        Tinkerとは

        Tinker は 対話型コマンドラインツールでphpの実行やデータベースの操作が可能です

        Tinkerの起動

        php artisan tinker

        DB接続を内容を確認

        # DB接続確認(以下のいずれかを実行)
        DB::connection()->getPdo();   # 接続成功ならPDOオブジェクトが表示
        DB::connection()->getDatabaseName();   # データベース名が表示
        
        # 実際のデータを確認
        DB::table('users')->get();   # usersテーブルの全データ表示

        テストデータの作成

        ① User.php ユーザーモデルの定義 nickname, email, password ② UserFactory.php テストデータの作り方 ダミーデータの生成ルール ③ DatabaseSeeder 10件のデータを作成 User::factory(10)->create() 実行コマンド php artisan db:seed

        Usersテーブルのマイグレーションファイルでテーブルの構造を定義

        マイグレーションファイルは「データベースの設計図」のようなものです。
        データベースの構造を定義

        database\migrations\0001_01_01_000000_create_users_table.phpを編集

        <?php
        
        use Illuminate\Database\Migrations\Migration;
        use Illuminate\Database\Schema\Blueprint;
        use Illuminate\Support\Facades\Schema;
        
        return new class extends Migration
        {
            /**
             * Run the migrations.
             */
            public function up(): void
            {
                Schema::create('users', function (Blueprint $table) {
                    $table->id();
                    $table->string('nickname', 100);
                    $table->string('email')->unique();
                    $table->string('password');
                    $table->tinyInteger('locked_flg')->default(0);
                    $table->integer('error_count')->unsigned()->default(0);
                    $table->timestamps();
                });
            }
        };

        Laravelには「マイグレーション」という機能があり、データベースのテーブル構造やカラムをPHPコードで定義し、コマンドを実行することでデータベースに反映します。

        php artisan migrate

        User.phpを編集しユーザー関連のロジックを定義

        app\Models\User.phpを編集

        <?php
        
        namespace App\Models;
        
        // use Illuminate\Contracts\Auth\MustVerifyEmail;
        use Illuminate\Database\Eloquent\Factories\HasFactory;
        use Illuminate\Foundation\Auth\User as Authenticatable;
        use Illuminate\Notifications\Notifiable;
        
        class User extends Authenticatable
        {
            /** @use HasFactory<\Database\Factories\UserFactory> */
            use HasFactory, Notifiable;
        
            /**
             * The attributes that are mass assignable.
             *
             * @var array<int, string>
             */
            protected $fillable = [
                'nickname',
                'email',
                'password',
                'lcoked_flg',
                'error_count',
            ];
        
            /**
             * The attributes that should be hidden for serialization.
             *
             * @var array<int, string>
             */
            protected $hidden = [
                'password',
            ];
        
            /**
             * Get the attributes that should be cast.
             *
             * @return array<string, string>
             */
            // protected function casts(): array
            // {
            //     return [
            //         'email_verified_at' => 'datetime',
            //         'password' => 'hashed',
            //     ];
            // }
        }
        

        UserFactory.phpでユーザーデータを作成

        database\factories\UserFactory.phpを編集

        <?php
        
        namespace Database\Factories;
        
        use Illuminate\Database\Eloquent\Factories\Factory;
        use Illuminate\Support\Facades\Hash;
        use Illuminate\Support\Str;
        
        /**
         * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
         */
        class UserFactory extends Factory
        {
            /**
             * The current password being used by the factory.
             */
            protected static ?string $password;
        
            /**
             * Define the model's default state.
             *
             * @return array<string, mixed>
             */
            public function definition(): array
            {
                return [
                    'nickname' => fake()->name(), // ランダムな名前を生成
                    'email' => fake()->unique()->safeEmail(), // ランダムでユニークなメールアドレスを生成
                    'password' => static::$password ??= Hash::make('password'), // パスワードをハッシュ化
        
                ];
            }
        }
        

        Laravelのデータベースシーディング(seeding)機能を使ってテストデータを作成

        database\seeders\DatabaseSeeder.phpを編集

        <?php
        
        namespace Database\Seeders;
        
        use App\Models\User;
        // use Illuminate\Database\Console\Seeds\WithoutModelEvents;
        use Illuminate\Database\Seeder;
        
        class DatabaseSeeder extends Seeder
        {
            /**
             * Seed the application's database.
             */
            public function run(): void
            {
                User::factory(10)->create();
        
                // User::factory()->create([
                //     'name' => 'Test User',
                //     'email' => 'test@example.com',
                // ]);
            }
        }
        

        php artisan db:seed コマンドは、データベースに初期データを挿入するためのコマンドです。

        php artisan db:seed

        ログイン画面の設定

        auth/login.blade.phpを作成

        <!DOCTYPE html>
        <html lang="ja">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>ログイン</title>
            <!-- Tailwind CSSを使用 -->
            @vite('resources/css/app.css')
        </head>
        <body class="bg-gray-100">
            <div class="min-h-screen flex items-center justify-center">
                <div class="bg-white p-8 rounded-lg shadow-md w-96">
                    <h1 class="text-2xl font-bold mb-8 text-center">ログイン</h1>
        
                    @if ($errors->any())
                        <div class="bg-red-100 text-red-700 p-4 mb-4 rounded">
                            <ul>
                                @foreach ($errors->all() as $error)
                                    <li>{{ $error }}</li>
                                @endforeach
                            </ul>
                        </div>
                    @endif
        
                    <form method="POST" action="{{ route('login') }}">
                        @csrf
                        
                        <!-- メールアドレス -->
                        <div class="mb-4">
                            <label for="email" class="block text-gray-700 mb-2">メールアドレス</label>
                            <input 
                                type="email" 
                                name="email" 
                                id="email" 
                                value="{{ old('email') }}"
                                class="w-full p-2 border rounded focus:outline-none focus:border-blue-500"
                                required 
                                autofocus
                                >
                        </div>
        
                        <!-- パスワード -->
                        <div class="mb-6">
                            <label for="password" class="block text-gray-700 mb-2">パスワード</label>
                            <input 
                                type="password" 
                                name="password" 
                                id="password" 
                                class="w-full p-2 border rounded focus:outline-none focus:border-blue-500"
                                required
                            >
                        </div>
        
                        <!-- ログインボタン -->
                        <div class="flex flex-col gap-4">
                            <button type="submit" 
                                    class="w-full py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none">
                                ログイン
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </body>
        </html>

        ログイン機能のルーティングを設定

        routes/web.phpを編集

        
        use App\Http\Controllers\Auth\LoginController;
        
        // ログイン関連のルート
        Route::middleware('guest')->group(function () {
            // ログイン画面表示
            Route::get('/login', [LoginController::class, 'showLoginForm'])
                ->name('login');
            
            // ログイン処理
            Route::post('/login', [LoginController::class, 'login']);
        });
        
        // ログアウト(認証済みユーザーのみ)
        Route::middleware('auth')->group(function () {
            // ダッシュボード表示
            Route::get('/dashboard', function () {
                return view('dashboard');
            })->name('dashboard');
        
            // ログアウト処理
            Route::post('/logout', [LoginController::class, 'logout'])
                ->name('logout');
        });

        Cursor始め方

        ダウンロード

        https://www.cursor.com

        インストーラーで初期設定

        ダウンロードしたインストーラを起動

        「Autocomplete Preferences(自動補完設定)」に関する設定画面

        • GH Copilot …従来のもの
        • CursorのCopilot++(デフォルト) …より強力なバージョンのCopilot(デフォルト)

        「Data Preferences(データ設定)」

        • データ提供
        • プライバシーモード

        ブラウザでサインイン画面を開く

        初期設定が完了すると、ブラウザが自動的にサインイン画面を開きます。ここでアカウントを作成するか、既存のアカウントでログインします。

        Cursorアプリケーション内でログイン

        最後にCursorアプリケーション内で「Log in」を選択し、ログインを完了させます。

        WinSCPのインストール方法

        インストール

        下記サイトより
        https://winscp.net/eng/download.php

        「DOWNLOAD WINSCP」をクリック

        ダウンロードしたインストーラーを実行

        自分のみの利用の為、「現在のユーザー用にインストール」

        使用許諾を「許諾」

        標準的なインストール

        インターフェースの設定は後から変更可能です

        表示 →環境設定

        インストール

        完了

        接続方法

        • プロトコル →SFTP(デフォルト)
        • ホスト名
        • ポート番号 →22(デフォルト)
        • ユーザ名
        • パスワード

        ssh接続を初回実行時、下記の警告が出ます

        承認すると、再度パスワード入力を求められますので、実施

        ディレクトリ同期

        ディレクトリ同期移動

        有効化すると左右同じディレクトリ階層で表示されます

        ファイルの編集でエディタをVScodeにしたい場合

        任意のファイルを「右クリック」→「編集」→「設定」に進みます

        エディタから外部エディタで参照でVSCodeのexeファイルを選択

        追加されたCode(VSCode)を上にをクリックし一番上に移動します

        隠しファイル表示する方法

        メニューバーの歯車(環境設定)からパネルを選択し、隠しファイルを表示するを✓

        参考サイト

        WinSCPとは?インストール方法や使い方を解説する【初心者向け】
        https://miyashimo-studio.jp/blog/detail/winscp-how-to-use/

        DockerでApacheのSSL接続

        こんにちは!今回は、Docker環境でのSSL接続設定について詳しく解説します。SSL(Secure Sockets Layer)は、インターネット上での通信を暗号化し、セキュリティを向上させる重要な技術です。Dockerを使用した開発環境でも、SSL接続を適切に設定することで、より本番環境に近い安全な環境を構築できます。

        自己署名証明書を使用する場合

        https://localhostとしてブラウザで確認すると警告がでます

        警告ページで「詳細」や「続行」などのオプションを探します。 リスクを理解した上で、「Webサイトへ進む」などのオプションを選択します。 ブラウザは警告を表示しつつも、ページの表示を許可します。

        注意点:この方法は開発環境でのみ推奨されます。

        1. docker-compose.ymlの設定

        まず、docker-compose.ymlファイルでSSL接続用のポートを公開する必要があります。以下のように設定します:

        services:
          app:
            ports:
              - "0.0.0.0:8082:80"
              - "443:443"

        この設定により、ホストマシンのポート443がDockerコンテナ内のポート443にマッピングされます。これにより、外部からのHTTPS接続(ポート443)をコンテナ内のApacheサーバーで受け付けることができます。

        ホストマシン(Dockerを実行しているPC) Dockerコンテナ 80 443 80 443 ポートマッピング: 80:80 と 443:443

        2. Dockerfileでの設定

        次に、Dockerfileで以下のようなSSL関連の設定を行います:

        1)SSLモジュールの有効化:

        RUN a2enmod ssl

        2)デフォルトのSSLサイト設定の有効化:

        RUN a2ensite default-ssl

        3)自己署名SSL証明書の生成:

        RUN echo '#!/bin/bash
        openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
        -keyout /etc/ssl/private/apache-selfsigned.key \
        -out /etc/ssl/certs/apache-selfsigned.crt \
        -subj "/CN=localhost"
        sed -i "s|SSLCertificateFile.*|SSLCertificateFile /etc/ssl/certs/apache-selfsigned.crt|" /etc/apache2/sites-available/default-ssl.conf
        sed -i "s|SSLCertificateKeyFile.*|SSLCertificateKeyFile /etc/ssl/private/apache-selfsigned.key|" /etc/apache2/sites-available/default-ssl.conf' > /usr/local/bin/generate_ssl_cert.sh
        
        RUN chmod +x /usr/local/bin/generate_ssl_cert.sh && /usr/local/bin/generate_ssl_cert.sh
        • opensslコマンドを使用して自己署名証明書を生成しています。
        • -x509:自己署名証明書を生成することを指定します。
        • -nodes:秘密鍵をパスワードで保護しないことを指定します。
        • -days 365:証明書の有効期間を1年に設定します。
        • -newkey rsa:2048:2048ビットのRSA鍵を新しく生成します。
        • -keyout-out:鍵と証明書の出力先を指定します。
        • -subj "/CN=localhost":証明書のサブジェクト(ここではCommon Name)を設定します。…証明書が保護するドメイン名または IP アドレスを指定します
        • sed は “stream editor” の略で、テキストの変換や置換に使用
          • 置換パターン:s|旧パターン|新パターン| の形式で、| はデリミタ(区切り文字)として機能

        4)ポート443の公開:

        EXPOSE 80 443

        PHP環境にLet’s Encryptを統合する手順

        ローカルDocker開発環境では難しい、、、?

        DockerPHP開発環境構築:mkcertを使用したSSL接続の設定

        mkcertを使用することで、自己署名証明書の警告なしに安全な開発環境を構築できます。

        1. mkcert「エムケイサート」のインストールと使用

        1)Windows PowerShell”を右クリックし、“管理者として実行”を選択します。

        2)Chocolateyがインストールされていない場合は、まずChocolateyをインストール

        Chocolateyは、Windowsのためのパッケージマネージャーです。

        Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

        3)mkcertをインストール:

        choco install mkcert

        インストールの確認

        mkcert --version

        4)ローカル認証局のインストール

        mkcert -install

        インストールの確認

        mkcert -CAROOT

        5)証明書の生成:
        プロジェクトディレクトリに移動し、以下のコマンドを実行:

        mkdir certs
        cd certs
        mkcert localhost 127.0.0.1 ::1

        →ファイル生成
        ・証明書ファイル: [最初のドメイン名]+[追加エントリ数].pem (公開鍵を含む)
        ・秘密鍵ファイル: [最初のドメイン名]+[追加エントリ数]-key.pem

        localhost+2.pem 証明書 (公開情報) 公開鍵 localhost+2-key.pem 秘密鍵 対応 秘密 – 共有しないこと!

        6)各ファイルを編集

        鍵ファイルを作成したら下記を実施します

        • Apache設定ファイルでは、SSLエンジンを有効にし、証明書と秘密鍵のパスを正しく指定しています。
        • Dockerfileでは、Apacheの SSL モジュールを有効化しています。
        • docker-compose.ymlでは、443ポートをホストにマッピングし、必要なボリュームをマウントしています。
        DockerPHP開発環境でのSSL設定(mkcert使用) ホストマシン Dockerコンテナ mkcert SSL証明書 Apache PHP 証明書のコピー 1. mkcertインストール 2. 証明書生成 3. Dockerfile設定 4. docker-compose.yml設定 5. Apache SSL設定 6. PHP設定(必要に応じて) 7. アプリケーション開発 HTTPSアクセス
        project_root/
        │
        ├── local-mysite/
        │   ├── certs/
        │   │   ├── localhost+2.pem
        │   │   └── localhost+2-key.pem
        │   ├── env_vars/
        │   │   └── localhost
        │   ├── sites-available/
        │   │   └── 000-default.conf
        │   ├── .htaccess
        │   ├── docker-compose.yml
        │   ├── Dockerfile
        │   ├── php.ini
        │   └── ssl_cert_gen.sh
        │
        └── mysite/
            └── (PHPアプリケーションファイル)

        ▼docker-compose.yml

        version: "3.4"
        services:
          app:
            # Dockerfileからイメージをビルド
            build:
              context: .
              dockerfile: Dockerfile
            
            # ボリュームマウントの設定
            volumes:
              # 環境変数ファイルをマウント
              - ./env_vars:/var/www/env_vars
              # PHPアプリケーションのソースコードをマウント
              - ../0824_mysite:/var/www/html
              # SSL証明書をマウント(読み取り専用)
              - ./certs/localhost+2.pem:/etc/ssl/certs/localhost+2.pem:ro
              # SSL秘密鍵をマウント(読み取り専用)
              - ./certs/localhost+2-key.pem:/etc/ssl/private/localhost+2-key.pem:ro
              # Apache設定ファイルをマウント
              - ./sites-available:/etc/apache2/sites-available
        
            # ポートマッピングの設定
            ports:
              # HTTP: ホストの8082ポートをコンテナの80ポートにマッピング
              - "0.0.0.0:8082:80"
              # HTTPS: ホストの443ポートをコンテナの443ポートにマッピング
              - "443:443"
        
            # 環境変数の設定(必要に応じて追加)
            environment:
              - APACHE_DOCUMENT_ROOT=/var/www/html

        ▼Dockerfile

        # SSL対応PHP開発環境のDockerfile
        
        # PHP 8.1とApacheをベースとした公式イメージを使用
        FROM php:8.1-apache
        
        # カスタムApache設定ファイルをコンテナ内の指定された場所にコピー
        COPY ./sites-available/000-default.conf /etc/apache2/sites-available/000-default.conf
        
        # PDO_MySQL拡張をインストールし、Apacheのrewrite、proxy、proxy_httpモジュールを有効化
        RUN docker-php-ext-install pdo_mysql && a2enmod rewrite proxy proxy_http
        
        # SSLモジュールを有効化
        RUN a2enmod ssl
        
        # デフォルトのSSLサイト設定を有効化
        RUN a2ensite default-ssl
        
        # 必要なパッケージをインストール
        RUN apt-get update && apt-get install -y \
            curl \
            gnupg \
            git \
            unzip \
            openssl \
            # Node.jsのセットアップスクリプトを取得して実行
            && curl -sL https://deb.nodesource.com/setup_18.x | bash - \
            # Node.jsをインストール
            && apt-get install -y nodejs \
            # パッケージリストを削除してイメージサイズを削減
            && rm -rf /var/lib/apt/lists/*
        
        # Composerをコンテナ内にコピー
        COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
        
        # コンテナ内の作業ディレクトリを設定
        WORKDIR /var/www/html
        
        # Composerの設定
        ENV COMPOSER_HOME /var/www/.composer
        RUN mkdir -p $COMPOSER_HOME && chown -R www-data:www-data $COMPOSER_HOME
        
        # Apache実行ユーザーの設定
        ENV APACHE_RUN_USER www-data
        ENV APACHE_RUN_GROUP www-data
        
        # Apacheの設定を変更し、.htaccessが機能するようにAllowOverrideをAllに設定
        RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
        
        # コンテナのポート80(HTTP)と443(HTTPS)を公開
        EXPOSE 80 443
        
        # .htaccessファイルをコンテナ内にコピー
        COPY .htaccess /var/www/html/.htaccess
        
        # .htaccessファイルの所有権と権限を設定
        RUN chown www-data:www-data /var/www/html/.htaccess && chmod 644 /var/www/html/.htaccess
        
        # カスタムphp.iniをコンテナ内にコピー
        COPY ./php.ini /usr/local/etc/php/php.ini
        
        # PHPエラーログファイルを作成し、適切な権限を設定
        RUN touch /var/log/php_errors.log && chmod 666 /var/log/php_errors.log
        
        # mkcertで生成した証明書をコンテナ内にコピー
        COPY ./certs/localhost+2.pem /etc/ssl/certs/localhost.crt
        COPY ./certs/localhost+2-key.pem /etc/ssl/private/localhost.key
        
        # 証明書のパーミッションを適切に設定
        RUN chmod 644 /etc/ssl/certs/localhost.crt && \
            chmod 600 /etc/ssl/private/localhost.key
        
        # Apacheをフォアグラウンドで実行
        CMD ["apache2-foreground"]

        ▼000-default.conf

        <VirtualHost *:80>
            ServerName localhost
            DocumentRoot /var/www/html
            Redirect permanent / https://localhost/
        
            # HTTPのアクセスログとエラーログ(必要に応じてコメントを外す)
            # ErrorLog ${APACHE_LOG_DIR}/error.log
            # CustomLog ${APACHE_LOG_DIR}/access.log combined
        </VirtualHost>
        
        <VirtualHost *:443>
            ServerName localhost
            DocumentRoot /var/www/html
            SSLEngine on
            SSLCertificateFile /etc/ssl/certs/localhost+2.pem
            SSLCertificateKeyFile /etc/ssl/private/localhost+2-key.pem
        
            <Directory /var/www/html>
                AllowOverride All
                Require all granted
            </Directory>
        
            ErrorLog ${APACHE_LOG_DIR}/error.log
            CustomLog ${APACHE_LOG_DIR}/access.log combined
        </VirtualHost>
        ```
        
        この設定は、HTTPからHTTPSへのリダイレクトと、SSL/TLS接続の基本的な設定を提供します。
        🐳 PHP 🔒 DockerPHP SSL設定

        mkcertとopenssl、Let’s Encryptを使用したSSL設定の比較

        mkcertとopenssl、Let’s Encryptを使用したSSL設定の主な違いは、使いやすさと生成される証明書の性質にあります。

        mkcertは開発環境に特化しており、簡単な操作で信頼されたSSL証明書を生成できます。一方、opensslはより汎用的で、詳細な設定が可能ですが、使用にはより深い知識が必要です。

        開発環境では、mkcertの使用が推奨されます:

        1. セットアップが簡単で、チーム全体で一貫した環境を構築しやすい。
        2. ブラウザ警告がなく、よりスムーズなテストが可能。
        3. 開発者が証明書の詳細な管理に時間を割く必要がない。

        一方、以下の場合はopensslの使用が適切です:

        1. 本番環境用の証明書生成。
        2. カスタムな証明書要件がある場合。
        3. 証明書生成プロセスの詳細な制御が必要な場合。

        重要なポイントは、開発環境と本番環境で異なるアプローチを取ることが多いということです。開発ではmkcertの簡便性を活かし、本番ではopensslやLet’s Encryptなどを使用して、より厳格なセキュリティを確保することが一般的です。

        SSL/TLS証明書ツールの比較 mkcert Let’s Encrypt OpenSSL ・ローカル開発用 ・簡単な操作 ・自動的にCA作成 ・ブラウザ警告なし ・無料の公的証明書 ・自動更新可能 ・Webサーバー必要 ・ドメイン名必須 ・汎用的なツール ・詳細な設定可能 ・自己署名証明書 ・高度な知識必要 開発環境 本番環境 カスタム要件

        セキュリティの考慮事項

        開発環境でのmkcert使用

        利点:

        1. 簡単にローカルでHTTPS環境を構築できる
        2. 開発者の生産性向上
        3. 本番環境に近い条件でのテストが可能

        制限:

        1. ローカルマシンでのみ信頼される証明書
        2. 公的に認証された証明書ではない

        mkcertはローカル環境に最適化されているため、本番環境特有の問題を見逃す可能性がある

        一般的な本番環境要件:

        1. 信頼された認証局(CA)による証明書の使用
        2. 強力な暗号化アルゴリズムの採用
        3. 定期的な証明書の更新
        4. 適切なサーバー設定(TLSバージョン、暗号スイートなど)
        「PHPMailer」使用手順、セキュリティ(機密情報設定ファイルは別配置等)、Docker開発から本番環境へアップロード

        ローカルDocker環境でPHPMailerを使用する初心者向けの手順

        1)プロジェクトディレクトリ

        project-dir
          ├dockerfile
          └docker-compose.yml

        2)Dockerfile作成

        Dockerfileについて

        Dockerfileに記述された指示に従って、Dockerイメージ(設計図)を作成します

        ▽Dockerfile

        3)docker-compose.yml作成

        version: '3'
        services:
          web:
            build: .
            ports:
              - "8080:80"
            volumes:
              - ./src:/var/www/html

        4)project-dirにて「docker-compose build「docker-compose up -d

        1. docker-compose build:
          • このコマンドは Dockerfile に基づいてイメージをビルドします。
        2. docker-compose up -d:
          • このコマンドはコンテナを起動し、バックグラウンドで実行します。
          • Docker Compose ファイルで定義されたボリュームをマウントします。
            ※もし指定されたホスト側のディレクトリ(この場合は src)が存在しない場合、Docker は自動的にそれを作成します。

        「docker desktop」でContainer作成が確認できます

        5)index.php send_mail.phpを作成

        project_root/
        ├── Dockerfile
        ├── docker-compose.yml
        └── src/
             ├── index.php
             └── send_mail.php
        

        ▽index.php

        <!DOCTYPE html>
        <html lang="ja">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>PHPMailerテスト</title>
        </head>
        <body>
            <h1>PHPMailerテスト</h1>
            <form action="send_mail.php" method="post">
                <label for="to">宛先:</label>
                <input type="email" id="to" name="to" required><br><br>
                
                <label for="subject">件名:</label>
                <input type="text" id="subject" name="subject" required><br><br>
                
                <label for="message">本文:</label><br>
                <textarea id="message" name="message" rows="4" cols="50" required></textarea><br><br>
                
                <input type="submit" value="送信">
            </form>
        </body>
        </html>

        ▽send_mail.php

        <?php
        
        use PHPMailer\PHPMailer\PHPMailer;// PHPMailerライブラリの読み込み
        use PHPMailer\PHPMailer\Exception;// PHPMailerライブラリの読み込み
        
        require 'vendor/autoload.php';// PHPMailerライブラリの読み込み
        
        if ($_SERVER["REQUEST_METHOD"] == "POST") {
            $mail = new PHPMailer(true); //PHPMailerのインスタンス作成(trueは例外を有効にする)
        
            try {
                //SMTPサーバー:Gmailの設定
                $mail->isSMTP();
                $mail->Host       = 'smtp.gmail.com';  // SMTPサーバーを指定
                $mail->SMTPAuth   = true;
                $mail->Username   = 'yourmail@gmail.com';  // SMTPユーザー
                $mail->Password   = 'your app pass';          // SMTPパスワード
                $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
                $mail->Port       = 465;
        
                //送信元、送信先の設定
                $mail->setFrom('yourmail@gmail.com', 'Mailer');
                $mail->addAddress($_POST['to']);
        
                //メール本文
                $mail->isHTML(true);
                $mail->Subject = $_POST['subject'];
                $mail->Body    = $_POST['message'];
        
                $mail->send();
                echo 'メッセージが送信されました';
        
            } catch (Exception $e) {
                echo "メッセージを送信できませんでした。Mailer Error: {$mail->ErrorInfo}";
            }
        }

        6)コンテナ内でPHPMailerをインストールします:

        docker-compose exec web composer require phpmailer/phpmailer

        7)Googleアプリパスワード生成

        Google→セキュリティ→2段階認証プロセス→アプリパスワードから設定

        send_mail.phpを編集

        注意)本番環境では、これらの設定を環境変数や別の設定ファイルに移動し、Gitなどのバージョン管理システムにコミットしないようにすることをおすすめします。

        セキュリティ面

        サニタイズについて

        サニタイズ(sanitize)とは、ユーザーから入力されたデータを安全使用可能な形式に変換することを指します。

        セキュリティヘッダ

        セキュリティを強化するために使用される特別なHTTPヘッダ

        • header(“X-XSS-Protection: 1; mode=block”); //ブラウザの組み込みのXSS対策フィルターを有効にし、攻撃を検出したら、ページの読み込みをブロックします。
        • header(“X-Frame-Options: SAMEORIGIN”); //ページを<frame>、<iframe>、<embed>、<object>で表示することを許可します。ただし、同じオリジンの場合のみ。(クリックジャッキング対策)
        • header(“X-Content-Type-Options: nosniff”); //ブラウザがコンテンツタイプをスニッフィングしないようにします。
        • header(“Referrer-Policy: strict-origin-when-cross-origin”); //クロスオリジンのリクエストに対しては、Referer ヘッダーにはリクエスト元のオリジンのみを含めます。
        • header(“Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘unsafe-inline’ ‘unsafe-eval’; style-src ‘self’ ‘unsafe-inline’;”); //様々な攻撃(XSS、データ注入など)からサイトを保護します。

        サニタイズ、セキュリティヘッダを導入のためPHPファイルを修正する

        ▽send_mail.php

        <?php
        // ob_start(); は、「出力を一時的に裏側で溜めておく」という命令
        // もし途中でエラーメッセージが表示されたり、予期せぬ出力があったりすると、セキュリティヘッダーを設定できなくなるため
        ob_start();
        
        // セッションが開始されていない場合のみ、設定を変更してセッションを開始
        if (session_status() == PHP_SESSION_NONE) {    // セッション関連の設定
            ini_set('session.cookie_httponly', 1);
            ini_set('session.cookie_secure', 1);        // セッションを開始
            session_start();
        } else {
            // セッションが既に開始されている場合は、そのまま続行
            session_start();
        }
        
        // CSRFトークンがセッションに存在しない場合は生成
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        
        // セキュリティヘッダーの設定
        header("X-XSS-Protection: 1; mode=block");
        header("X-Frame-Options: SAMEORIGIN");
        header("X-Content-Type-Options: nosniff");
        header("Referrer-Policy: strict-origin-when-cross-origin");
        header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';");
        
        use PHPMailer\PHPMailer\PHPMailer;// PHPMailerライブラリの読み込み
        use PHPMailer\PHPMailer\Exception;// PHPMailerライブラリの読み込み
        
        require 'vendor/autoload.php';// PHPMailerライブラリの読み込み
        
        if ($_SERVER["REQUEST_METHOD"] == "POST") {
        
            // CSRFトークンの検証
            if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
                // エラーをログに記録
                error_log("CSRF token mismatch. POST token: " . ($_POST['csrf_token'] ?? 'not set') . ", Session token: " . ($_SESSION['csrf_token'] ?? 'not set'));
                die('セッションが期限切れか無効です。ページを更新して再度お試しください。');
            }
        
            // 入力のサニタイズとバリデーション
            $to = filter_var($_POST['to'], FILTER_SANITIZE_EMAIL);
            $subject = htmlspecialchars($_POST['subject'], ENT_QUOTES, 'UTF-8');
            $message = htmlspecialchars($_POST['message'], ENT_QUOTES, 'UTF-8');
        
            // メールアドレスの妥当性チェック
            if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
                die('無効なメールアドレスです。');
            }
        
            $mail = new PHPMailer(true); //PHPMailerのインスタンス作成(trueは例外を有効にする)
        
            try {
                //SMTPサーバー:Gmailの設定
                $mail->isSMTP();
                $mail->Host       = 'smtp.gmail.com';  // SMTPサーバーを指定
                $mail->SMTPAuth   = true;
                $mail->Username   = 'yourmail@gmail.com';  // SMTPユーザー
                $mail->Password   = 'your app pass';          // SMTPパスワード
                $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
                $mail->Port       = 465;
        
                //送信元、送信先の設定
                $mail->setFrom('yourmail@gmail.com', 'Mailer');
                $mail->addAddress($to);
        
                //メール本文
                $mail->Subject = $subject;
                $mail->Body    = $message;
                $mail->AltBody = strip_tags($message); //HTMLタグを除去
        
                $mail->send();
                echo 'メッセージが送信されました';
        
            } catch (Exception $e) {
                // 詳細なエラー情報を隠し、ログに記録します。
                error_log("メール送信エラー: " . $mail->ErrorInfo);
                echo "メッセージを送信できませんでした。管理者にお問い合わせください。";
            }
        
            // 新しいCSRFトークンの生成
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        
        } else {
            // POSTメソッド以外でアクセスされた場合の処理
            die('不正なアクセスです。');
        }
        
        // 出力バッファリングを終了し、出力を送信
        ob_end_flush();

        ▽index.php

        <?php
        session_start();
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        } // CSRFトークンがセッションに存在しない場合は生成
        ?>

        機密情報を含む設定ファイルを作成

        メリット

        • セキュリティ向上:機密情報(パスワードなど)をGitリポジトリにコミットせずに済みます。
          (.gitignoreに追加すると)
        • ポータビリティの向上:異なるサーバーや環境への移行が容易

        1)プロジェクトのルートディレクトリに configフォルダその中にconfig.php を作成します

        project-dir\config\config.php

        <?php
        define('SMTP_HOST', getenv('SMTP_HOST') ?: 'smtp.example.com');
        define('SMTP_USER', getenv('SMTP_USER') ?: 'user@example.com');
        define('SMTP_PASS', getenv('SMTP_PASS') ?: 'password');

        2)上記修正に伴いsend_mail.php、docker-compose.ymlも修正

        ▽send_mail.php

        …
        
        require 'vendor/autoload.php';// PHPMailerライブラリの読み込み
        require_once __DIR__ . '/../config/config.php';
        …
        
        
                //SMTPサーバー:Gmailの設定
                $mail->isSMTP();
                $mail->Host       = SMTP_HOST; // SMTPサーバーを指定
                $mail->Username   = SMTP_USER; // SMTPユーザー
                $mail->Password   = SMTP_PASS; // SMTPパスワード
                $mail->SMTPAuth   = true;
                $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
                $mail->Port       = 465;

        ▽docker-compose.yml

            volumes:
              - ./src:/var/www/html
              - ./config:/var/www/config.php

        3)2)の修正を行った後、Dockerコンテナを再ビルドして起動

        docker-compose down
        docker-compose build
        docker-compose up -d

        2)プロジェクトのルートディレクトリに .gitignore ファイルを作成

        ▽.gitignore

        /config/config.php

        本番環境のさくらインターネットへアップロード

        ローカルDocker開発環境、さくらインターネットの本番環境の両環境併用できるよう下記の通り修正

        ▽config.php

        <?php
        // 環境の判別
        $is_local = ($_SERVER['SERVER_NAME'] == 'localhost' || $_SERVER['SERVER_ADDR'] == '127.0.0.1');
        
        // デバッグモードの設定
        define('DEBUG_MODE', $is_local); // ローカルではデバッグモードON、本番では OFF
        // 本番環境で一時的にデバッグモードを有効化する場合
        // define('DEBUG_MODE', true);  // コメントを外して使用
        
        
        if ($is_local) {
            // ローカル環境(Docker)の設定
            define('SMTP_HOST', getenv('SMTP_HOST') ?: 'smtp.gmail.com');
            define('SMTP_USER', getenv('SMTP_USER') ?: 'xxxx@gmail.com');
            define('SMTP_PASS', getenv('SMTP_PASS') ?: 'xxxx');
            define('SMTP_PORT', 587);
            define('SMTP_SECURE', 'tls');
        } else {
            // さくらインターネット環境の設定
            define('SMTP_HOST', 'xxxx');
            define('SMTP_USER', 'xxxx');
            define('SMTP_PASS', 'xxxx');
            define('SMTP_PORT', 587);
            define('SMTP_SECURE', 'tls');
        }
        
        // サイトの URL
        define('SITE_URL', $is_local ? 'http://localhost:8080' : 'xxxx');

        さくらインターネットのメール情報の確認方法の参考サイト

        下記のホスト、ユーザ名、パスワードについて

        • define(‘SMTP_HOST’, ‘xxxx’);
        • define(‘SMTP_USER’, ‘xxxx’);
        • define(‘SMTP_PASS’, ‘xxxx’);

        さくらのメールボックスを PHPMailer で SMTP + STARTTLS で送信する時の注意点https://qiita.com/ameyamashiro/items/c7283bd1ec5dd3146ef9

        ▽send_mail.php

        <?php
        // ob_start(); は、「出力を一時的に裏側で溜めておく」という命令
        // もし途中でエラーメッセージが表示されたり、予期せぬ出力があったりすると、セキュリティヘッダーを設定できなくなるため
        ob_start();
        
        // 設定ファイルの読み込み
        if (strpos(__DIR__, '/home/siennahare23') !== false) {
            // さくらインターネット環境
            require_once '/home/siennahare23/config/config.php';
        } else {
            // ローカル環境
            require_once __DIR__ . '/../config/config.php';
        }
        
        // セッションが開始されていない場合のみ、設定を変更してセッションを開始
        if (session_status() == PHP_SESSION_NONE) {
            // セッション関連の設定
            ini_set('session.cookie_httponly', 1);
            ini_set('session.cookie_secure', 1);
            
            // セッションを開始
            session_start();
        } else {
            // セッションが既に開始されている場合は、そのまま続行
            session_start();
        }
        
        // CSRFトークンがセッションに存在しない場合は生成
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        
        // セキュリティヘッダーの設定
        header("X-XSS-Protection: 1; mode=block"); //ブラウザの組み込みのXSS対策フィルターを有効にし、攻撃を検出したら、ページの読み込みをブロックします。
        header("X-Frame-Options: SAMEORIGIN"); //ページを<frame>、<iframe>、<embed>、<object>で表示することを許可します。ただし、同じオリジンの場合のみ。(クリックジャッキング対策)
        header("X-Content-Type-Options: nosniff"); //ブラウザがコンテンツタイプをスニッフィングしないようにします。
        header("Referrer-Policy: strict-origin-when-cross-origin"); //クロスオリジンのリクエストに対しては、Referer ヘッダーにはリクエスト元のオリジンのみを含めます。
        header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"); //様々な攻撃(XSS、データ注入など)からサイトを保護します。
        
        use PHPMailer\PHPMailer\PHPMailer;// PHPMailerライブラリの読み込み
        use PHPMailer\PHPMailer\Exception;// PHPMailerライブラリの読み込み
        
        require 'vendor/autoload.php';// PHPMailerライブラリの読み込み
        
        if ($_SERVER["REQUEST_METHOD"] == "POST") {
        
            // CSRFトークンの検証
            if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
                // エラーをログに記録
                error_log("CSRF token mismatch. POST token: " . ($_POST['csrf_token'] ?? 'not set') . ", Session token: " . ($_SESSION['csrf_token'] ?? 'not set'));
                die('セッションが期限切れか無効です。ページを更新して再度お試しください。');
            }
        
            // 入力のサニタイズとバリデーション
            $to = filter_var($_POST['to'], FILTER_SANITIZE_EMAIL);
            $subject = htmlspecialchars($_POST['subject'], ENT_QUOTES, 'UTF-8');
            $message = htmlspecialchars($_POST['message'], ENT_QUOTES, 'UTF-8');
        
            // メールアドレスの妥当性チェック
            if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
                die('無効なメールアドレスです。');
            }
        
            $mail = new PHPMailer(true); //PHPMailerのインスタンス作成(trueは例外を有効にする)
        
            // デバッグモードの設定
            // $mail->SMTPDebug = 3; // デバッグ出力を有効化
            $mail->Debugoutput = 'html'; // デバッグ出力形式をHTMLに設定
        
            try {
                //SMTPサーバー:Gmailの設定
                $mail->isSMTP();
                $mail->Host       = SMTP_HOST; // SMTPサーバーを指定
                $mail->Username   = SMTP_USER; // SMTPユーザー
                $mail->Password   = SMTP_PASS; // SMTPパスワード
                $mail->SMTPAuth   = true;
                $mail->SMTPSecure = SMTP_SECURE;
                $mail->Port       = SMTP_PORT;
        
                //送信元、送信先の設定
                $mail->setFrom(SMTP_USER, 'Mailer');
                $mail->addAddress($to);
        
                //メール本文
                $mail->isHTML(true);
                $mail->Subject = $subject;
                $mail->Body    = $message;
                $mail->AltBody = strip_tags($message); //HTMLタグを除去
        
                $mail->send();
                echo 'メッセージが送信されました';
        
            } catch (Exception $e) {
        
                if (DEBUG_MODE) {
                    echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}<br>";
                    echo "Detailed error: " . $e->getMessage();
                } else {
                    echo "メッセージを送信できませんでした。管理者にお問い合わせください。";
                }
                error_log("メール送信エラー: " . $mail->ErrorInfo);
            }
        
            // 新しいCSRFトークンの生成
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        
        } else {
            // POSTメソッド以外でアクセスされた場合の処理
            die('不正なアクセスです。');
        }
        
        // 出力バッファリングを終了し、出力を送信
        ob_end_flush();

        アップロード

        <ローカルのディレクトリ構成>

        project_root/
        │
        ├── src/
        │   ├── index.php
        │   ├── send_mail.php
        │   └── vendor/ ...
        ├── config/
        └── config.php
        │
        └── docker-compose.yml

        <本番環境のディレクトリ構成>

        /home/xxxx/www/xxxx/
        │
        ├── contact/
        │  │
        │   ├── index.php
        │   ├── send_mail.php
        │   └── vendor/ ...
        │
        └── config/
            └── config.php

        実際のフォーム

        「ローカルとさくらインターネット」をssh接続、「さくらインターネットとGitHub」をssh接続の手順

        「ローカルとさくらインターネット」をssh接続の手順

        本手順はパスワード認証方式

        パスワード認証方式公開鍵認証方式
        手段パスワード「公開鍵」など生成したファイル
        安全度低い
        パスワード流出で第3者にログインされる恐れがあります
        高い
        作業について簡単複雑

        1)Winキー + R →「CMD」と入力ターミナルを起動

        2)「ssh ユーザー名@サーバー名」と入力

        C:\Users\jingt>ssh shiennahare23@siennahare23.sakura.ne.jp
        The authenticity of host 'siennahare23.sakura.ne.jp (163.43.87.150)' can't be established.
        ED25519 key fingerprint is SHA256:6UflOXoaum1XWseZ2Xj8SYFcGI7lnEw1p42zjlc/Egk.
        This key is not known by any other names
        Are you sure you want to continue connecting (yes/no/[fingerprint])?

        このメッセージは、SSHを使用して初めて接続しようとしたときに表示されるものです。サーバーのED25519公開鍵のフィンガープリントが表示され、このサーバーが信頼できるものかどうかを確認しています。

        →接続先サーバに設定された公開鍵のフィンガープリントが表示されるので、問題がなければ”yes”を入力します。

        Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
        Warning: Permanently added 'siennahare23.sakura.ne.jp' (ED25519) to the list of known hosts.
        Connection closed by 163.43.87.150 port 22

        3)サーバーパスワードの入力

        siennahare23@siennahare23.sakura.ne.jp's password:

        パスワードを要求されるのでパスワードを入力

        サーバーパスワードの確認:
        https://help.sakura.ad.jp/rs/2243

        Welcome to FreeBSD!

        →と表示されログインできます

        4)exitで接続終了

        % exit
        logout
        Connection to siennahare23.sakura.ne.jp closed.

        「さくらインターネットとGitHub」をssh接続の手順

        1)bashに変更

        % bash
        [siennahare23@www3910 ~]$

        ※bashに切り替える理由は分かりません…

        2)公開鍵と秘密鍵を作成

        [siennahare23@www3910 ~]$ cd ~/.ssh
        [siennahare23@www3910 ~/.ssh]$ ssh-keygen -t rsa -b 4096

        鍵の名前、パスフレーズを要求されますが、「Enter」で進めるとデフォルトの名前(「id_rsa」、「id_rsa.pub」)でパスフレーズを必要としない鍵のペアが生成されます

        Generating public/private rsa key pair.en -t rsa -b 4096
        Enter file in which to save the key (/home/siennahare23/.ssh/id_rsa):
        Enter passphrase (empty for no passphrase):
        Enter same passphrase again:
        Your identification has been saved in /home/siennahare23/.ssh/id_rsa.
        Your public key has been saved in /home/siennahare23/.ssh/id_rsa.pub.
        The key fingerprint is:
        SHA256:3x+egZ5oS58YhHEoWZbiDnzFg+00qH2/y/Da9G1olgQ siennahare23@www3910.sakura.ne.jp
        The key's randomart image is:
        +---[RSA 4096]----+
        |       =o.       |
        |      ++O.       |
        |   . +o=oo.      |
        |    + +.o+E      |
        |     + .S...     |
        |      .  o....   |
        |        . =oooo  |
        |         BoB=*.+ |
        |        .oO=*.=  |
        +----[SHA256]-----+

        これで、SSH接続やGitHubとの認証に使用できる鍵ペアが正常に生成されました。

        3)Githubに公開鍵を登録

        公開鍵の内容を表示し、全文コピーします

        cat ~/.ssh/id_rsa.pub

        Githubの「Settings」→「SSH and GPG keys」→「New SSH key」をクリック

        コピーした公開鍵の内容をはりつけ

        4)「config」ファイルの作成

        [siennahare23@www3910 ~/.ssh]$ touch ~/.ssh/config

        ▽config

        Host github.com
          HostName github.com
          IdentityFile ~/.ssh/id_rsa
          User git

        ※秘密鍵の名前を変更している場合はデフォルトの「id_rsa」から変更する必要があります

        5)接続

        [siennahare23@www3910 ~]$ ssh -T git@github.com

        SSH 鍵が正しく設定され、GitHub に登録されている公開鍵と一致したことを示しています。

        Hi ida240609! You've successfully authenticated, but GitHub does not provide shell access.

        “but GitHub does not provide shell access.” – これは通常のメッセージです。GitHub は直接のシェルアクセスを提供していないことを説明しています。つまり、対話的なシェルセッションは使用できませんが、Git 操作(push, pull など)には問題なく SSH を使用できます。

        これで、SSH を使用して GitHub リポジトリのクローン、プッシュ、プルなどの操作を行うことができます。

        例)

        git clone git@github.com:username/repository.git

        のようなコマンドが使用可能です(username と repository は実際のものに置き換えてください)。

        参考サイト

        https://saunabouya.com/2022/10/18/sakura-github-ssh

        【VSCode連携】「GitHub Copilot」使い方

        「GitHub Copilot」の特徴

        VSCodeと連携可能(その他エディターも)

        「GitHub Copilot」の費用

        GitHub Copilot Individual サブスクリプションは、月単位または年単位のサイクルで利用できます。

        • 毎月の支払いサイクルを選択すると、カレンダー月ごとに 10 米国ドル が課金されます。
        • 年単位の支払いサイクルを選択した場合、年間 100 米ドル が課金されます。

        GitHub Copilot の課金
        https://docs.github.com/ja/billing/managing-billing-for-github-copilot/about-billing-for-github-copilot

        「GitHub Copilot」の始め方

        1)プロフィールアイコンをクリック、メニューから「Settings」をクリック

        2)「Copilot」をクリック、「Start free trial」をクリック

        3)プランを選択し、「Get access to GitHub Copilot」をクリック

        4)名前、住所を入力し、「Save」

        5)クレジットカード等支払い方法情報を入力

        6)登録情報を確認後「Submit」をクリック

        Select Your preferencesで好みの設定

        • 「Suggestions matching public code…」ではGithubのパブリックコードの使用を許可するかで、特にルールがなければ「Allowed」でOK
        • 「Allow GitHub to use my code snippets from …」自分のコードをGithubが読み取ってよいかの設定です
          ※チェックしたとしてもプライベートコードは公開されません

        以上で完了です

        「GitHub Copilot」の解約方法

        解約方法もGithubのページ、ユーザアイコンをクリックし設定より可能です

        参考サイト

        https://app-tatsujin.com/what-is-githubcopilot-how-to-start
        Rubyインストール手順(Windows)

        Rubyインストーラーをダウンロード

        RubyInstallerのダウンロードページ
        https://rubyinstaller.org/downloads

        PCのビット数を確認し、インストーラーを選択

        ▽設定→システム→バージョン情報より確認できます

        インストーラーを実行

        1)「Install for me only」をクリック

        2)同意して「Next」をクリック

        3)下記の設定(デフォルトのまま)「Install」をクリック

        4)下記の設定(デフォルトのまま)「Next」をクリック

        →インストールが開始されます

        5)数分後、下記画面が表示されインストール終了

        →「Finish」をクリックするとインストーラーの画面は消えてターミナルが表示されます

        「MSYS2」のインストール

        「MSYS2」… Ruby開発で便利なツール

        「1」、「Enter」を押下するとインストールが開始されます

        続いて「2」、「Enter」を押下、最新版にアップデート

        最後に「3」、「Enter」を押下、3つ目のメニュー実行

        完了したら「Enter」をクリックして「MSYS2」のインストール完了

        Rubyインストールされているか確認

        ターミナルにてバージョンを確認→表示されれば正しくインストールされています

        C:\Users\xxx>ruby -v
        ruby 3.3.3 (2024-06-12 revision f1c7b6f435) [x64-mingw-ucrt]

        参考サイト

        https://web-camp.io/magazine/archives/15051
        「Github Page」にReactを公開する方法

        1)Reactプロジェクトを作成

        npx create-react-app app-dir

        2)Branchの発行し、GitHubのリポジトリを作成

        VS Codeの場合「Branchの発行」をクリックしリポジトリ名を入力する

        3)Reactアプリのビルド

        npm run build

        →buildディレクトリが作成されます

        4)gh-pagesをインストール

        Reactアプリを簡単にデプロイできる「gh-pages」ツールをインストール

        npm install --save gh-pages

        package.jsonを編集

        ▽package.jsonにて「homepageフィールド」の追加、「デプロイスクリプト」の追加

        {
          "homepage": "https://<GitHubアカウント名>.github.io/<GitHubリポジトリ名>/",
          "name": "app-dir",
          "version": "0.1.0",
          "private": true,
          "dependencies": {
            "@testing-library/jest-dom": "^5.17.0",
            "@testing-library/react": "^13.4.0",
            "@testing-library/user-event": "^13.5.0",
            "react": "^18.3.1",
            "react-dom": "^18.3.1",
            "react-scripts": "5.0.1",
            "web-vitals": "^2.1.4"
          },
          "scripts": {
            "start": "react-scripts start",
            "build": "react-scripts build",
            "predeploy": "npm run build",
            "deploy": "gh-pages -d build",
            "eject": "react-scripts eject"
          },
          "eslintConfig": {
            "extends": [
              "react-app",
              "react-app/jest"
            ]
          },
          "browserslist": {
            "production": [
              ">0.2%",
              "not dead",
              "not op_mini all"
            ],
            "development": [
              "last 1 chrome version",
              "last 1 firefox version",
              "last 1 safari version"
            ]
          }
        }

        「npm run deploy」を実行する

        cd app-dir
        npm run deploy

        公開URLについて

        "homepage": "https://github0612.github.io/app-dir0612/",

        「Branchの発行」後に作成されるリポジトリにてGitHubでBranchを「Save」をクリックすると数分後URLが表示されます

        ▽数分後URLが表示されます

        ▽サイトにて表示確認できます

        参考サイト

        GitHub Pages でReact Appを公開する方法
        https://note.com/wecken/n/n73196eb22a51

        ReactアプリをGithub Pagesにデプロイする方法
        https://qiita.com/snow_swallow/items/8455dd135b81fe0ce25f